001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2008-2010 Sun Microsystems, Inc. 015 * Portions Copyright 2011-2016 ForgeRock AS. 016 */ 017package org.opends.server.util.cli; 018 019import static com.forgerock.opendj.cli.Utils.*; 020 021import static org.opends.messages.ToolMessages.*; 022 023import java.io.PrintStream; 024import java.util.LinkedList; 025import java.util.Set; 026import java.util.concurrent.atomic.AtomicInteger; 027 028import javax.net.ssl.SSLException; 029 030import org.forgerock.i18n.LocalizableMessage; 031import org.opends.server.admin.client.cli.SecureConnectionCliArgs; 032import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler; 033import org.opends.server.tools.LDAPConnection; 034import org.opends.server.tools.LDAPConnectionException; 035import org.opends.server.tools.LDAPConnectionOptions; 036import org.opends.server.tools.SSLConnectionException; 037import org.opends.server.tools.SSLConnectionFactory; 038import org.opends.server.types.OpenDsException; 039 040import com.forgerock.opendj.cli.Argument; 041import com.forgerock.opendj.cli.ArgumentException; 042import com.forgerock.opendj.cli.ArgumentGroup; 043import com.forgerock.opendj.cli.ArgumentParser; 044import com.forgerock.opendj.cli.ClientException; 045import com.forgerock.opendj.cli.ConsoleApplication; 046import com.forgerock.opendj.cli.FileBasedArgument; 047import com.forgerock.opendj.cli.StringArgument; 048 049/** 050 * Creates an argument parser pre-populated with arguments for specifying 051 * information for opening and LDAPConnection an LDAP connection. 052 */ 053public class LDAPConnectionArgumentParser extends ArgumentParser 054{ 055 private SecureConnectionCliArgs args; 056 057 /** 058 * Creates a new instance of this argument parser with no arguments. Unnamed 059 * trailing arguments will not be allowed. 060 * 061 * @param mainClassName 062 * The fully-qualified name of the Java class that should be invoked 063 * to launch the program with which this argument parser is 064 * associated. 065 * @param toolDescription 066 * A human-readable description for the tool, which will be included 067 * when displaying usage information. 068 * @param longArgumentsCaseSensitive 069 * Indicates whether long arguments should 070 * @param argumentGroup 071 * Group to which LDAP arguments will be added to the parser. May be 072 * null to indicate that arguments should be added to the default 073 * group 074 * @param alwaysSSL 075 * If true, always use the SSL connection type. In this case, the 076 * arguments useSSL and startTLS are not present. 077 */ 078 public LDAPConnectionArgumentParser(String mainClassName, LocalizableMessage toolDescription, 079 boolean longArgumentsCaseSensitive, ArgumentGroup argumentGroup, boolean alwaysSSL) 080 { 081 super(mainClassName, toolDescription, longArgumentsCaseSensitive); 082 addLdapConnectionArguments(argumentGroup, alwaysSSL); 083 setVersionHandler(new DirectoryServerVersionHandler()); 084 } 085 086 /** 087 * Indicates whether the user has indicated that they would like to 088 * perform a remote operation based on the arguments. 089 * 090 * @return true if the user wants to perform a remote operation; false 091 * otherwise 092 */ 093 public boolean connectionArgumentsPresent() 094 { 095 return args != null && args.argumentsPresent(); 096 } 097 098 /** 099 * Creates a new LDAPConnection and invokes a connect operation using 100 * information provided in the parsed set of arguments that were provided by 101 * the user. 102 * 103 * @param out 104 * stream to write messages 105 * @param err 106 * stream to write error messages 107 * @return LDAPConnection created by this class from parsed arguments 108 * @throws LDAPConnectionException 109 * if there was a problem connecting to the server indicated by the 110 * input arguments 111 * @throws ArgumentException 112 * if there was a problem processing the input arguments 113 */ 114 public LDAPConnection connect(PrintStream out, PrintStream err) throws LDAPConnectionException, ArgumentException 115 { 116 return connect(this.args, out, err); 117 } 118 119 /** 120 * Creates a new LDAPConnection and invokes a connect operation using 121 * information provided in the parsed set of arguments that were provided by 122 * the user. 123 * 124 * @param args 125 * with which to connect 126 * @param out 127 * stream to write messages 128 * @param err 129 * stream to write error messages 130 * @return LDAPConnection created by this class from parsed arguments 131 * @throws LDAPConnectionException 132 * if there was a problem connecting to the server indicated by the 133 * input arguments 134 * @throws ArgumentException 135 * if there was a problem processing the input arguments 136 */ 137 private LDAPConnection connect(SecureConnectionCliArgs args, PrintStream out, PrintStream err) 138 throws LDAPConnectionException, ArgumentException 139 { 140 throwIfArgumentsConflict(args.getBindPasswordArg(), args.getBindPasswordFileArg()); 141 throwIfArgumentsConflict(args.getKeyStorePasswordArg(), args.getKeyStorePasswordFileArg()); 142 throwIfArgumentsConflict(args.getTrustStorePasswordArg(), args.getTrustStorePasswordFileArg()); 143 throwIfArgumentsConflict(args.getUseSSLArg(), args.getUseStartTLSArg()); 144 145 // Create the LDAP connection options object, which will be used to 146 // customize the way that we connect to the server and specify a set of 147 // basic defaults. 148 LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions(); 149 connectionOptions.setVersionNumber(3); 150 151 // See if we should use SSL or StartTLS when establishing the connection. 152 // If so, then make sure only one of them was specified. 153 if (args.getUseSSLArg().isPresent()) 154 { 155 connectionOptions.setUseSSL(true); 156 } 157 else if (args.getUseStartTLSArg().isPresent()) 158 { 159 connectionOptions.setStartTLS(true); 160 } 161 162 // If we should blindly trust any certificate, then install the appropriate 163 // SSL connection factory. 164 if (args.getUseSSLArg().isPresent() || args.getUseStartTLSArg().isPresent()) 165 { 166 try 167 { 168 String clientAlias; 169 if (args.getCertNicknameArg().isPresent()) 170 { 171 clientAlias = args.getCertNicknameArg().getValue(); 172 } 173 else 174 { 175 clientAlias = null; 176 } 177 178 SSLConnectionFactory sslConnectionFactory = new SSLConnectionFactory(); 179 sslConnectionFactory.init(args.getTrustAllArg().isPresent(), 180 args.getKeyStorePathArg().getValue(), 181 args.getKeyStorePasswordArg().getValue(), 182 clientAlias, 183 args.getTrustStorePathArg().getValue(), 184 args.getTrustStorePasswordArg().getValue()); 185 connectionOptions.setSSLConnectionFactory(sslConnectionFactory); 186 } 187 catch (SSLConnectionException sce) 188 { 189 printWrappedText(err, ERR_LDAP_CONN_CANNOT_INITIALIZE_SSL.get(sce.getMessage())); 190 } 191 } 192 193 // If one or more SASL options were provided, then make sure that one of 194 // them was "mech" and specified a valid SASL mechanism. 195 if (args.getSaslOptionArg().isPresent()) 196 { 197 String mechanism = null; 198 LinkedList<String> options = new LinkedList<>(); 199 200 for (String s : args.getSaslOptionArg().getValues()) 201 { 202 int equalPos = s.indexOf('='); 203 if (equalPos <= 0) 204 { 205 printAndThrowException(err, ERR_LDAP_CONN_CANNOT_PARSE_SASL_OPTION.get(s)); 206 } 207 else 208 { 209 String name = s.substring(0, equalPos); 210 if ("mech".equalsIgnoreCase(name)) 211 { 212 mechanism = s; 213 } 214 else 215 { 216 options.add(s); 217 } 218 } 219 } 220 221 if (mechanism == null) 222 { 223 printAndThrowException(err, ERR_LDAP_CONN_NO_SASL_MECHANISM.get()); 224 } 225 226 connectionOptions.setSASLMechanism(mechanism); 227 for (String option : options) 228 { 229 connectionOptions.addSASLProperty(option); 230 } 231 } 232 233 int timeout = args.getConnectTimeoutArg().getIntValue(); 234 235 final String passwordValue = getPasswordValue( 236 args.getBindPasswordArg(), args.getBindPasswordFileArg(), args.getBindDnArg(), out, err); 237 return connect( 238 args.getHostNameArg().getValue(), 239 args.getPortArg().getIntValue(), 240 args.getBindDnArg().getValue(), 241 passwordValue, 242 connectionOptions, timeout, out, err); 243 } 244 245 private void printAndThrowException(PrintStream err, LocalizableMessage message) throws ArgumentException 246 { 247 printWrappedText(err, message); 248 throw new ArgumentException(message); 249 } 250 251 /** 252 * Creates a connection using a console interaction that will be used to 253 * potentially interact with the user to prompt for necessary information for 254 * establishing the connection. 255 * 256 * @param ui 257 * user interaction for prompting the user 258 * @param out 259 * stream to write messages 260 * @param err 261 * stream to write error messages 262 * @return LDAPConnection created by this class from parsed arguments 263 * @throws SSLConnectionException 264 * if there was a problem connecting with SSL to the server 265 * @throws LDAPConnectionException 266 * if there was any other problem connecting to the server 267 * @throws ArgumentException 268 * if there was a problem indicated by the input arguments 269 */ 270 public LDAPConnection connect(LDAPConnectionConsoleInteraction ui, PrintStream out, PrintStream err) 271 throws LDAPConnectionException, SSLConnectionException, ArgumentException 272 { 273 try 274 { 275 ui.run(); 276 LDAPConnectionOptions options = new LDAPConnectionOptions(); 277 options.setVersionNumber(3); 278 return connect(ui.getHostName(), ui.getPortNumber(), ui.getBindDN(), 279 ui.getBindPassword(), ui.populateLDAPOptions(options), ui.getConnectTimeout(), out, err); 280 } 281 catch (OpenDsException e) 282 { 283 err.println(isSSLException(e) ? 284 ERR_TASKINFO_LDAP_EXCEPTION_SSL.get(ui.getHostName(), ui.getPortNumber()) : e.getMessageObject()); 285 throw e; 286 } 287 } 288 289 private boolean isSSLException(Exception e) 290 { 291 return e.getCause() != null 292 && e.getCause().getCause() != null 293 && e.getCause().getCause() instanceof SSLException; 294 } 295 296 /** 297 * Creates a connection from information provided. 298 * 299 * @param host 300 * of the server 301 * @param port 302 * of the server 303 * @param bindDN 304 * with which to connect 305 * @param bindPw 306 * with which to connect 307 * @param options 308 * with which to connect 309 * @param timeout 310 * the timeout to establish the connection in milliseconds. Use 311 * {@code 0} to express no timeout 312 * @param out 313 * stream to write messages 314 * @param err 315 * stream to write error messages 316 * @return LDAPConnection created by this class from parsed arguments 317 * @throws LDAPConnectionException 318 * if there was a problem connecting to the server indicated by the 319 * input arguments 320 */ 321 private LDAPConnection connect(String host, int port, String bindDN, String bindPw, LDAPConnectionOptions options, 322 int timeout, PrintStream out, PrintStream err) throws LDAPConnectionException 323 { 324 // Attempt to connect and authenticate to the Directory Server. 325 AtomicInteger nextMessageID = new AtomicInteger(1); 326 LDAPConnection connection = new LDAPConnection(host, port, options, out, err); 327 connection.connectToHost(bindDN, bindPw, nextMessageID, timeout); 328 return connection; 329 } 330 331 /** 332 * Gets the arguments associated with this parser. 333 * 334 * @return arguments for this parser. 335 */ 336 public SecureConnectionCliArgs getArguments() 337 { 338 return args; 339 } 340 341 /** 342 * Commodity method that retrieves the password value analyzing the contents 343 * of a string argument and of a file based argument. It assumes that the 344 * arguments have already been parsed and validated. If the string is a dash, 345 * or no password is available, it will prompt for it on the command line. 346 * 347 * @param bindPwdArg 348 * the string argument for the password. 349 * @param bindPwdFileArg 350 * the file based argument for the password. 351 * @param bindDnArg 352 * the string argument for the bindDN. 353 * @param out 354 * stream to write message. 355 * @param err 356 * stream to write error message. 357 * @return the password value. 358 */ 359 public static String getPasswordValue(StringArgument bindPwdArg, FileBasedArgument bindPwdFileArg, 360 StringArgument bindDnArg, PrintStream out, PrintStream err) 361 { 362 try 363 { 364 return getPasswordValue(bindPwdArg, bindPwdFileArg, bindDnArg.getValue(), out, err); 365 } 366 catch (Exception ex) 367 { 368 printWrappedText(err, ex.getMessage()); 369 return null; 370 } 371 } 372 373 /** 374 * Commodity method that retrieves the password value analyzing the contents 375 * of a string argument and of a file based argument. It assumes that the 376 * arguments have already been parsed and validated. If the string is a dash, 377 * or no password is available, it will prompt for it on the command line. 378 * 379 * @param bindPassword 380 * the string argument for the password. 381 * @param bindPasswordFile 382 * the file based argument for the password. 383 * @param bindDNValue 384 * the string value for the bindDN. 385 * @param out 386 * stream to write message. 387 * @param err 388 * stream to write error message. 389 * @return the password value. 390 * @throws ClientException 391 * if the password cannot be read 392 */ 393 public static String getPasswordValue(StringArgument bindPassword, FileBasedArgument bindPasswordFile, 394 String bindDNValue, PrintStream out, PrintStream err) throws ClientException 395 { 396 String bindPasswordValue = bindPassword.getValue(); 397 if ("-".equals(bindPasswordValue) 398 || (!bindPasswordFile.isPresent() && bindDNValue != null && bindPasswordValue == null)) 399 { 400 // read the password from the stdin. 401 out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(bindDNValue)); 402 char[] pwChars = ConsoleApplication.readPassword(); 403 // As per rfc 4513(section-5.1.2) a client should avoid sending 404 // an empty password to the server. 405 while (pwChars.length == 0) 406 { 407 printWrappedText(err, INFO_LDAPAUTH_NON_EMPTY_PASSWORD.get()); 408 out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(bindDNValue)); 409 pwChars = ConsoleApplication.readPassword(); 410 } 411 return new String(pwChars); 412 } 413 else if (bindPasswordValue == null) 414 { 415 // Read from file if it exists. 416 return bindPasswordFile.getValue(); 417 } 418 return bindPasswordValue; 419 } 420 421 private void addLdapConnectionArguments(ArgumentGroup argGroup, boolean alwaysSSL) 422 { 423 args = new SecureConnectionCliArgs(alwaysSSL); 424 try 425 { 426 Set<Argument> argSet = args.createGlobalArguments(); 427 for (Argument arg : argSet) 428 { 429 addArgument(arg, argGroup); 430 } 431 } 432 catch (ArgumentException ae) 433 { 434 ae.printStackTrace(); // Should never happen 435 } 436 } 437}