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