1b28a5538SAdam Hornacek /* 2b28a5538SAdam Hornacek * CDDL HEADER START 3b28a5538SAdam Hornacek * 4b28a5538SAdam Hornacek * The contents of this file are subject to the terms of the 5b28a5538SAdam Hornacek * Common Development and Distribution License (the "License"). 6b28a5538SAdam Hornacek * You may not use this file except in compliance with the License. 7b28a5538SAdam Hornacek * 8b28a5538SAdam Hornacek * See LICENSE.txt included in this distribution for the specific 9b28a5538SAdam Hornacek * language governing permissions and limitations under the License. 10b28a5538SAdam Hornacek * 11b28a5538SAdam Hornacek * When distributing Covered Code, include this CDDL HEADER in each 12b28a5538SAdam Hornacek * file and include the License file at LICENSE.txt. 13b28a5538SAdam Hornacek * If applicable, add the following below this CDDL HEADER, with the 14b28a5538SAdam Hornacek * fields enclosed by brackets "[]" replaced with your own identifying 15b28a5538SAdam Hornacek * information: Portions Copyright [yyyy] [name of copyright owner] 16b28a5538SAdam Hornacek * 17b28a5538SAdam Hornacek * CDDL HEADER END 18b28a5538SAdam Hornacek */ 19b28a5538SAdam Hornacek 20b28a5538SAdam Hornacek /* 21*c6f0939bSAdam Hornacek * Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. 22b28a5538SAdam Hornacek */ 23b28a5538SAdam Hornacek package opengrok.auth.plugin.ldap; 24b28a5538SAdam Hornacek 2514c8a3ffSVladimir Kotal import java.io.IOException; 26b28a5538SAdam Hornacek import java.io.Serializable; 2714c8a3ffSVladimir Kotal import java.net.InetAddress; 2814c8a3ffSVladimir Kotal import java.net.InetSocketAddress; 2914c8a3ffSVladimir Kotal import java.net.Socket; 3014c8a3ffSVladimir Kotal import java.net.URI; 3114c8a3ffSVladimir Kotal import java.net.URISyntaxException; 3214c8a3ffSVladimir Kotal import java.net.UnknownHostException; 33b28a5538SAdam Hornacek import java.util.Hashtable; 34b28a5538SAdam Hornacek import java.util.logging.Level; 35b28a5538SAdam Hornacek import java.util.logging.Logger; 36b28a5538SAdam Hornacek import javax.naming.CommunicationException; 37b28a5538SAdam Hornacek import javax.naming.Context; 38b28a5538SAdam Hornacek import javax.naming.NamingEnumeration; 39b28a5538SAdam Hornacek import javax.naming.NamingException; 40b28a5538SAdam Hornacek import javax.naming.directory.SearchControls; 41b28a5538SAdam Hornacek import javax.naming.directory.SearchResult; 42b28a5538SAdam Hornacek import javax.naming.ldap.InitialLdapContext; 43b28a5538SAdam Hornacek import javax.naming.ldap.LdapContext; 44b28a5538SAdam Hornacek 45b28a5538SAdam Hornacek public class LdapServer implements Serializable { 46b28a5538SAdam Hornacek 47b28a5538SAdam Hornacek private static final long serialVersionUID = -1; 48b28a5538SAdam Hornacek 49b28a5538SAdam Hornacek private static final Logger LOGGER = Logger.getLogger(LdapServer.class.getName()); 50b28a5538SAdam Hornacek 51d0624dbbSVladimir Kotal private static final String LDAP_CONNECT_TIMEOUT_PARAMETER = "com.sun.jndi.ldap.connect.timeout"; 52d0624dbbSVladimir Kotal private static final String LDAP_READ_TIMEOUT_PARAMETER = "com.sun.jndi.ldap.read.timeout"; 53b28a5538SAdam Hornacek private static final String LDAP_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; 54d0624dbbSVladimir Kotal 55d0624dbbSVladimir Kotal // default connectTimeout value in milliseconds 56d0624dbbSVladimir Kotal private static final int LDAP_CONNECT_TIMEOUT = 5000; 57d0624dbbSVladimir Kotal // default readTimeout value in milliseconds 58d0624dbbSVladimir Kotal private static final int LDAP_READ_TIMEOUT = 3000; 59b28a5538SAdam Hornacek 60b28a5538SAdam Hornacek private String url; 61b28a5538SAdam Hornacek private String username; 62b28a5538SAdam Hornacek private String password; 63b28a5538SAdam Hornacek private int connectTimeout; 64d0624dbbSVladimir Kotal private int readTimeout; 65b28a5538SAdam Hornacek private int interval = 10 * 1000; 66b28a5538SAdam Hornacek 67*c6f0939bSAdam Hornacek private final Hashtable<String, String> env; 68b28a5538SAdam Hornacek private LdapContext ctx; 69b28a5538SAdam Hornacek private long errorTimestamp = 0; 70b28a5538SAdam Hornacek LdapServer()71b28a5538SAdam Hornacek public LdapServer() { 72b28a5538SAdam Hornacek this(prepareEnv()); 73b28a5538SAdam Hornacek } 74b28a5538SAdam Hornacek LdapServer(String server)75b28a5538SAdam Hornacek public LdapServer(String server) { 76b28a5538SAdam Hornacek this(prepareEnv()); 7714c8a3ffSVladimir Kotal setName(server); 78b28a5538SAdam Hornacek } 79b28a5538SAdam Hornacek LdapServer(String server, String username, String password)80efd6ebf0SVladimir Kotal public LdapServer(String server, String username, String password) { 81efd6ebf0SVladimir Kotal this(prepareEnv()); 8214c8a3ffSVladimir Kotal setName(server); 83efd6ebf0SVladimir Kotal this.username = username; 84efd6ebf0SVladimir Kotal this.password = password; 85efd6ebf0SVladimir Kotal } 86efd6ebf0SVladimir Kotal LdapServer(Hashtable<String, String> env)87b28a5538SAdam Hornacek public LdapServer(Hashtable<String, String> env) { 88b28a5538SAdam Hornacek this.env = env; 89b28a5538SAdam Hornacek } 90b28a5538SAdam Hornacek getUrl()91b28a5538SAdam Hornacek public String getUrl() { 92b28a5538SAdam Hornacek return url; 93b28a5538SAdam Hornacek } 94b28a5538SAdam Hornacek setName(String name)95b28a5538SAdam Hornacek public LdapServer setName(String name) { 96b28a5538SAdam Hornacek this.url = name; 97b28a5538SAdam Hornacek return this; 98b28a5538SAdam Hornacek } 99b28a5538SAdam Hornacek getUsername()100b28a5538SAdam Hornacek public String getUsername() { 101b28a5538SAdam Hornacek return username; 102b28a5538SAdam Hornacek } 103b28a5538SAdam Hornacek setUsername(String username)104b28a5538SAdam Hornacek public LdapServer setUsername(String username) { 105b28a5538SAdam Hornacek this.username = username; 106b28a5538SAdam Hornacek return this; 107b28a5538SAdam Hornacek } 108b28a5538SAdam Hornacek getPassword()109b28a5538SAdam Hornacek public String getPassword() { 110b28a5538SAdam Hornacek return password; 111b28a5538SAdam Hornacek } 112b28a5538SAdam Hornacek setPassword(String password)113b28a5538SAdam Hornacek public LdapServer setPassword(String password) { 114b28a5538SAdam Hornacek this.password = password; 115b28a5538SAdam Hornacek return this; 116b28a5538SAdam Hornacek } 117b28a5538SAdam Hornacek getConnectTimeout()118b28a5538SAdam Hornacek public int getConnectTimeout() { 119b28a5538SAdam Hornacek return connectTimeout; 120b28a5538SAdam Hornacek } 121b28a5538SAdam Hornacek setConnectTimeout(int connectTimeout)122b28a5538SAdam Hornacek public LdapServer setConnectTimeout(int connectTimeout) { 123b28a5538SAdam Hornacek this.connectTimeout = connectTimeout; 124b28a5538SAdam Hornacek return this; 125b28a5538SAdam Hornacek } 126b28a5538SAdam Hornacek getReadTimeout()127d0624dbbSVladimir Kotal public int getReadTimeout() { 128d0624dbbSVladimir Kotal return readTimeout; 129d0624dbbSVladimir Kotal } 130d0624dbbSVladimir Kotal setReadTimeout(int readTimeout)131d0624dbbSVladimir Kotal public LdapServer setReadTimeout(int readTimeout) { 132d0624dbbSVladimir Kotal this.readTimeout = readTimeout; 133d0624dbbSVladimir Kotal return this; 134d0624dbbSVladimir Kotal } 135d0624dbbSVladimir Kotal getInterval()136b28a5538SAdam Hornacek public int getInterval() { 137b28a5538SAdam Hornacek return interval; 138b28a5538SAdam Hornacek } 139b28a5538SAdam Hornacek setInterval(int interval)140b28a5538SAdam Hornacek public void setInterval(int interval) { 141b28a5538SAdam Hornacek this.interval = interval; 142b28a5538SAdam Hornacek } 143b28a5538SAdam Hornacek urlToHostname(String urlStr)14414c8a3ffSVladimir Kotal private String urlToHostname(String urlStr) throws URISyntaxException { 14514c8a3ffSVladimir Kotal URI uri = new URI(urlStr); 14614c8a3ffSVladimir Kotal return uri.getHost(); 14714c8a3ffSVladimir Kotal } 14814c8a3ffSVladimir Kotal 149b28a5538SAdam Hornacek /** 15014c8a3ffSVladimir Kotal * This method converts the scheme from URI to port number. 15114c8a3ffSVladimir Kotal * It is limited to the ldap/ldaps schemes. 15214c8a3ffSVladimir Kotal * The method could be static however then it cannot be easily mocked in testing. 15314c8a3ffSVladimir Kotal * @return port number or -1 if the scheme in given URI is not known 15414c8a3ffSVladimir Kotal * @throws URISyntaxException if the URI is not valid 15514c8a3ffSVladimir Kotal */ getPort()15614c8a3ffSVladimir Kotal public int getPort() throws URISyntaxException { 15714c8a3ffSVladimir Kotal URI uri = new URI(getUrl()); 15814c8a3ffSVladimir Kotal switch (uri.getScheme()) { 15914c8a3ffSVladimir Kotal case "ldaps": 16014c8a3ffSVladimir Kotal return 636; 16114c8a3ffSVladimir Kotal case "ldap": 16214c8a3ffSVladimir Kotal return 389; 16314c8a3ffSVladimir Kotal } 16414c8a3ffSVladimir Kotal 16514c8a3ffSVladimir Kotal return -1; 16614c8a3ffSVladimir Kotal } 16714c8a3ffSVladimir Kotal isReachable(InetAddress addr, int port, int timeOutMillis)16814c8a3ffSVladimir Kotal private boolean isReachable(InetAddress addr, int port, int timeOutMillis) { 16914c8a3ffSVladimir Kotal try { 17014c8a3ffSVladimir Kotal try (Socket soc = new Socket()) { 17114c8a3ffSVladimir Kotal soc.connect(new InetSocketAddress(addr, port), timeOutMillis); 17214c8a3ffSVladimir Kotal } 17314c8a3ffSVladimir Kotal return true; 17414c8a3ffSVladimir Kotal } catch (IOException e) { 17514c8a3ffSVladimir Kotal return false; 17614c8a3ffSVladimir Kotal } 17714c8a3ffSVladimir Kotal } 17814c8a3ffSVladimir Kotal 17914c8a3ffSVladimir Kotal /** 18014c8a3ffSVladimir Kotal * Wraps InetAddress.getAllByName() so that it can be mocked in testing. 18114c8a3ffSVladimir Kotal * (mocking static methods is not really possible with Mockito) 18214c8a3ffSVladimir Kotal * @param hostname hostname string 18314c8a3ffSVladimir Kotal * @return array of InetAddress objects 18414c8a3ffSVladimir Kotal * @throws UnknownHostException if the host cannot be resolved to any IP address 18514c8a3ffSVladimir Kotal */ getAddresses(String hostname)18614c8a3ffSVladimir Kotal public InetAddress[] getAddresses(String hostname) throws UnknownHostException { 18714c8a3ffSVladimir Kotal return InetAddress.getAllByName(hostname); 18814c8a3ffSVladimir Kotal } 18914c8a3ffSVladimir Kotal 19014c8a3ffSVladimir Kotal /** 19114c8a3ffSVladimir Kotal * Go through all IP addresses and find out if they are reachable. 19214c8a3ffSVladimir Kotal * @return true if all IP addresses are reachable, false otherwise 19314c8a3ffSVladimir Kotal */ isReachable()19414c8a3ffSVladimir Kotal public boolean isReachable() { 19514c8a3ffSVladimir Kotal try { 19614c8a3ffSVladimir Kotal InetAddress[] addresses = getAddresses(urlToHostname(getUrl())); 19714c8a3ffSVladimir Kotal if (addresses.length == 0) { 19814c8a3ffSVladimir Kotal LOGGER.log(Level.WARNING, "LDAP server {0} does not resolve to any IP address", this); 19914c8a3ffSVladimir Kotal return false; 20014c8a3ffSVladimir Kotal } 20114c8a3ffSVladimir Kotal 20214c8a3ffSVladimir Kotal for (InetAddress addr : addresses) { 20314c8a3ffSVladimir Kotal // InetAddr.isReachable() is not sufficient as it can only check ICMP and TCP echo. 20414c8a3ffSVladimir Kotal int port = getPort(); 20514c8a3ffSVladimir Kotal if (!isReachable(addr, port, getConnectTimeout())) { 20614c8a3ffSVladimir Kotal LOGGER.log(Level.WARNING, "LDAP server {0} is not reachable on {1}:{2}", 20714c8a3ffSVladimir Kotal new Object[]{this, addr, Integer.toString(port)}); 20814c8a3ffSVladimir Kotal return false; 20914c8a3ffSVladimir Kotal } 21014c8a3ffSVladimir Kotal } 21114c8a3ffSVladimir Kotal } catch (UnknownHostException e) { 21214c8a3ffSVladimir Kotal LOGGER.log(Level.SEVERE, String.format("cannot get IP addresses for LDAP server %s", this), e); 21314c8a3ffSVladimir Kotal return false; 21414c8a3ffSVladimir Kotal } catch (URISyntaxException e) { 21514c8a3ffSVladimir Kotal LOGGER.log(Level.SEVERE, String.format("not a valid URI: %s", getUrl()), e); 21614c8a3ffSVladimir Kotal return false; 21714c8a3ffSVladimir Kotal } 21814c8a3ffSVladimir Kotal 21914c8a3ffSVladimir Kotal return true; 22014c8a3ffSVladimir Kotal } 22114c8a3ffSVladimir Kotal 22214c8a3ffSVladimir Kotal /** 22314c8a3ffSVladimir Kotal * The LDAP server is working only when it is reachable and its connection is not null. 22414c8a3ffSVladimir Kotal * This method tries to establish the connection if it is not established already. 225b28a5538SAdam Hornacek * 226b28a5538SAdam Hornacek * @return true if it is working 227b28a5538SAdam Hornacek */ isWorking()228b28a5538SAdam Hornacek public synchronized boolean isWorking() { 229d1d45fe1SVladimir Kotal if (ctx == null) { 23014c8a3ffSVladimir Kotal if (!isReachable()) { 23114c8a3ffSVladimir Kotal return false; 23214c8a3ffSVladimir Kotal } 23314c8a3ffSVladimir Kotal 234b28a5538SAdam Hornacek ctx = connect(); 235b28a5538SAdam Hornacek } 236b28a5538SAdam Hornacek return ctx != null; 237b28a5538SAdam Hornacek } 238b28a5538SAdam Hornacek 239b28a5538SAdam Hornacek /** 240b28a5538SAdam Hornacek * Connects to the LDAP server. 241b28a5538SAdam Hornacek * 242b28a5538SAdam Hornacek * @return the new connection or null 243b28a5538SAdam Hornacek */ connect()244b28a5538SAdam Hornacek private synchronized LdapContext connect() { 245*c6f0939bSAdam Hornacek LOGGER.log(Level.INFO, "Connecting to LDAP server {0} ", this); 246b28a5538SAdam Hornacek 247b28a5538SAdam Hornacek if (errorTimestamp > 0 && errorTimestamp + interval > System.currentTimeMillis()) { 24814c8a3ffSVladimir Kotal LOGGER.log(Level.WARNING, "LDAP server {0} is down", this.url); 249b28a5538SAdam Hornacek close(); 250b28a5538SAdam Hornacek return null; 251b28a5538SAdam Hornacek } 252b28a5538SAdam Hornacek 253b28a5538SAdam Hornacek if (ctx == null) { 254b28a5538SAdam Hornacek env.put(Context.PROVIDER_URL, this.url); 255b28a5538SAdam Hornacek 256b28a5538SAdam Hornacek if (this.username != null) { 257b28a5538SAdam Hornacek env.put(Context.SECURITY_PRINCIPAL, this.username); 258b28a5538SAdam Hornacek } 259b28a5538SAdam Hornacek if (this.password != null) { 260b28a5538SAdam Hornacek env.put(Context.SECURITY_CREDENTIALS, this.password); 261b28a5538SAdam Hornacek } 262b28a5538SAdam Hornacek if (this.connectTimeout > 0) { 263d0624dbbSVladimir Kotal env.put(LDAP_CONNECT_TIMEOUT_PARAMETER, Integer.toString(this.connectTimeout)); 264d0624dbbSVladimir Kotal } 265d0624dbbSVladimir Kotal if (this.readTimeout > 0) { 266d0624dbbSVladimir Kotal env.put(LDAP_READ_TIMEOUT_PARAMETER, Integer.toString(this.readTimeout)); 267b28a5538SAdam Hornacek } 268b28a5538SAdam Hornacek 269b28a5538SAdam Hornacek try { 270b28a5538SAdam Hornacek ctx = new InitialLdapContext(env, null); 271b28a5538SAdam Hornacek ctx.setRequestControls(null); 272*c6f0939bSAdam Hornacek LOGGER.log(Level.INFO, "Connected to LDAP server {0}", this); 273b28a5538SAdam Hornacek errorTimestamp = 0; 274b28a5538SAdam Hornacek } catch (NamingException ex) { 27514c8a3ffSVladimir Kotal LOGGER.log(Level.WARNING, "LDAP server {0} is not responding", env.get(Context.PROVIDER_URL)); 276b28a5538SAdam Hornacek errorTimestamp = System.currentTimeMillis(); 277b28a5538SAdam Hornacek close(); 278b28a5538SAdam Hornacek return ctx = null; 279b28a5538SAdam Hornacek } 280b28a5538SAdam Hornacek } 281b28a5538SAdam Hornacek 282b28a5538SAdam Hornacek return ctx; 283b28a5538SAdam Hornacek } 284b28a5538SAdam Hornacek 285b28a5538SAdam Hornacek /** 286b28a5538SAdam Hornacek * Lookups the LDAP server. 287b28a5538SAdam Hornacek * 288b28a5538SAdam Hornacek * @param name base dn for the search 289b28a5538SAdam Hornacek * @param filter LDAP filter 290b28a5538SAdam Hornacek * @param cons controls for the LDAP request 291b28a5538SAdam Hornacek * @return LDAP enumeration with the results 292b28a5538SAdam Hornacek * 293b28a5538SAdam Hornacek * @throws NamingException naming exception 294b28a5538SAdam Hornacek */ search(String name, String filter, SearchControls cons)295b28a5538SAdam Hornacek public NamingEnumeration<SearchResult> search(String name, String filter, SearchControls cons) throws NamingException { 296b28a5538SAdam Hornacek return search(name, filter, cons, false); 297b28a5538SAdam Hornacek } 298b28a5538SAdam Hornacek 299b28a5538SAdam Hornacek /** 30014c8a3ffSVladimir Kotal * Perform LDAP search. 301b28a5538SAdam Hornacek * 302b28a5538SAdam Hornacek * @param name base dn for the search 303b28a5538SAdam Hornacek * @param filter LDAP filter 304b28a5538SAdam Hornacek * @param controls controls for the LDAP request 305b28a5538SAdam Hornacek * @param reconnected flag if the request has failed previously 306b28a5538SAdam Hornacek * @return LDAP enumeration with the results 307b28a5538SAdam Hornacek * 308b28a5538SAdam Hornacek * @throws NamingException naming exception 309b28a5538SAdam Hornacek */ search(String name, String filter, SearchControls controls, boolean reconnected)310b28a5538SAdam Hornacek public NamingEnumeration<SearchResult> search(String name, String filter, SearchControls controls, boolean reconnected) 311b28a5538SAdam Hornacek throws NamingException { 312b28a5538SAdam Hornacek 313b28a5538SAdam Hornacek if (!isWorking()) { 314b28a5538SAdam Hornacek close(); 315b28a5538SAdam Hornacek throw new CommunicationException(String.format("LDAP server \"%s\" is down", 316b28a5538SAdam Hornacek env.get(Context.PROVIDER_URL))); 317b28a5538SAdam Hornacek } 318b28a5538SAdam Hornacek 319b28a5538SAdam Hornacek if (reconnected) { 320b28a5538SAdam Hornacek LOGGER.log(Level.INFO, "LDAP server {0} reconnect", env.get(Context.PROVIDER_URL)); 321b28a5538SAdam Hornacek close(); 322b28a5538SAdam Hornacek if ((ctx = connect()) == null) { 323b28a5538SAdam Hornacek throw new CommunicationException(String.format("LDAP server \"%s\" cannot reconnect", 324b28a5538SAdam Hornacek env.get(Context.PROVIDER_URL))); 325b28a5538SAdam Hornacek } 326b28a5538SAdam Hornacek } 327b28a5538SAdam Hornacek 328b28a5538SAdam Hornacek try { 329b28a5538SAdam Hornacek synchronized (this) { 330b28a5538SAdam Hornacek return ctx.search(name, filter, controls); 331b28a5538SAdam Hornacek } 332b28a5538SAdam Hornacek } catch (CommunicationException ex) { 333b28a5538SAdam Hornacek if (reconnected) { 334b28a5538SAdam Hornacek throw ex; 335b28a5538SAdam Hornacek } 336b28a5538SAdam Hornacek return search(name, filter, controls, true); 337b28a5538SAdam Hornacek } 338b28a5538SAdam Hornacek } 339b28a5538SAdam Hornacek 340b28a5538SAdam Hornacek /** 341b28a5538SAdam Hornacek * Closes the server context. 342b28a5538SAdam Hornacek */ close()343b28a5538SAdam Hornacek public synchronized void close() { 344b28a5538SAdam Hornacek if (ctx != null) { 345b28a5538SAdam Hornacek try { 346b28a5538SAdam Hornacek ctx.close(); 347b28a5538SAdam Hornacek } catch (NamingException ex) { 348b28a5538SAdam Hornacek LOGGER.log(Level.WARNING, "cannot close LDAP server {0}", getUrl()); 349b28a5538SAdam Hornacek } 350b28a5538SAdam Hornacek ctx = null; 351b28a5538SAdam Hornacek } 352b28a5538SAdam Hornacek } 353b28a5538SAdam Hornacek prepareEnv()354b28a5538SAdam Hornacek private static Hashtable<String, String> prepareEnv() { 355*c6f0939bSAdam Hornacek Hashtable<String, String> e = new Hashtable<>(); 356b28a5538SAdam Hornacek 357b28a5538SAdam Hornacek e.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_CONTEXT_FACTORY); 358d0624dbbSVladimir Kotal e.put(LDAP_CONNECT_TIMEOUT_PARAMETER, Integer.toString(LDAP_CONNECT_TIMEOUT)); 359d0624dbbSVladimir Kotal e.put(LDAP_READ_TIMEOUT_PARAMETER, Integer.toString(LDAP_READ_TIMEOUT)); 360b28a5538SAdam Hornacek 361b28a5538SAdam Hornacek return e; 362b28a5538SAdam Hornacek } 36317deb9edSVladimir Kotal 36417deb9edSVladimir Kotal @Override toString()36517deb9edSVladimir Kotal public String toString() { 366efd6ebf0SVladimir Kotal StringBuilder sb = new StringBuilder(); 367efd6ebf0SVladimir Kotal 368efd6ebf0SVladimir Kotal sb.append(getUrl()); 369efd6ebf0SVladimir Kotal 370efd6ebf0SVladimir Kotal if (getConnectTimeout() > 0) { 371d0624dbbSVladimir Kotal sb.append(", connect timeout: "); 372efd6ebf0SVladimir Kotal sb.append(getConnectTimeout()); 373efd6ebf0SVladimir Kotal } 374d0624dbbSVladimir Kotal if (getReadTimeout() > 0) { 375d0624dbbSVladimir Kotal sb.append(", read timeout: "); 376d0624dbbSVladimir Kotal sb.append(getReadTimeout()); 377d0624dbbSVladimir Kotal } 378efd6ebf0SVladimir Kotal 379efd6ebf0SVladimir Kotal if (getUsername() != null && !getUsername().isEmpty()) { 380d0624dbbSVladimir Kotal sb.append(", username: "); 381efd6ebf0SVladimir Kotal sb.append(getUsername()); 382efd6ebf0SVladimir Kotal } 383efd6ebf0SVladimir Kotal 384efd6ebf0SVladimir Kotal return sb.toString(); 38517deb9edSVladimir Kotal } 386b28a5538SAdam Hornacek } 387