001/*
002 * Copyright 2011-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2011-2017 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.util;
022
023
024
025import java.lang.reflect.InvocationTargetException;
026import java.lang.reflect.Method;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.TreeMap;
033
034import com.unboundid.ldap.sdk.ANONYMOUSBindRequest;
035import com.unboundid.ldap.sdk.Control;
036import com.unboundid.ldap.sdk.CRAMMD5BindRequest;
037import com.unboundid.ldap.sdk.DIGESTMD5BindRequest;
038import com.unboundid.ldap.sdk.DIGESTMD5BindRequestProperties;
039import com.unboundid.ldap.sdk.EXTERNALBindRequest;
040import com.unboundid.ldap.sdk.GSSAPIBindRequest;
041import com.unboundid.ldap.sdk.GSSAPIBindRequestProperties;
042import com.unboundid.ldap.sdk.LDAPException;
043import com.unboundid.ldap.sdk.PLAINBindRequest;
044import com.unboundid.ldap.sdk.ResultCode;
045import com.unboundid.ldap.sdk.SASLBindRequest;
046import com.unboundid.ldap.sdk.SASLQualityOfProtection;
047
048import static com.unboundid.util.StaticUtils.*;
049import static com.unboundid.util.UtilityMessages.*;
050
051
052
053/**
054 * This class provides a utility that may be used to help process SASL bind
055 * operations using the LDAP SDK.
056 */
057@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
058public final class SASLUtils
059{
060  /**
061   * The name of the SASL option that specifies the authentication ID.  It may
062   * be used in conjunction with the CRAM-MD5, DIGEST-MD5, GSSAPI, and PLAIN
063   * mechanisms.
064   */
065  public static final String SASL_OPTION_AUTH_ID = "authID";
066
067
068
069  /**
070   * The name of the SASL option that specifies the authorization ID.  It may
071   * be used in conjunction with the DIGEST-MD5, GSSAPI, and PLAIN mechanisms.
072   */
073  public static final String SASL_OPTION_AUTHZ_ID = "authzID";
074
075
076
077  /**
078   * The name of the SASL option that specifies the path to the JAAS config
079   * file.  It may be used in conjunction with the GSSAPI mechanism.
080   */
081  public static final String SASL_OPTION_CONFIG_FILE = "configFile";
082
083
084
085  /**
086   * The name of the SASL option that indicates whether debugging should be
087   * enabled.  It may be used in conjunction with the GSSAPI mechanism.
088   */
089  public static final String SASL_OPTION_DEBUG = "debug";
090
091
092
093  /**
094   * The name of the SASL option that specifies the KDC address.  It may be used
095   * in conjunction with the GSSAPI mechanism.
096   */
097  public static final String SASL_OPTION_KDC_ADDRESS = "kdcAddress";
098
099
100
101
102  /**
103   * The name of the SASL option that specifies the desired SASL mechanism to
104   * use to authenticate to the server.
105   */
106  public static final String SASL_OPTION_MECHANISM = "mech";
107
108
109
110  /**
111   * The name of the SASL option that specifies the GSSAPI service principal
112   * protocol.  It may be used in conjunction with the GSSAPI mechanism.
113   */
114  public static final String SASL_OPTION_PROTOCOL = "protocol";
115
116
117
118  /**
119   * The name of the SASL option that specifies the quality of protection that
120   * should be used for communication that occurs after the authentication has
121   * completed.
122   */
123  public static final String SASL_OPTION_QOP = "qop";
124
125
126
127  /**
128   * The name of the SASL option that specifies the realm name.  It may be used
129   * in conjunction with the DIGEST-MD5 and GSSAPI mechanisms.
130   */
131  public static final String SASL_OPTION_REALM = "realm";
132
133
134
135  /**
136   * The name of the SASL option that indicates whether to require an existing
137   * Kerberos session from the ticket cache.  It may be used in conjunction with
138   * the GSSAPI mechanism.
139   */
140  public static final String SASL_OPTION_REQUIRE_CACHE = "requireCache";
141
142
143
144  /**
145   * The name of the SASL option that indicates whether to attempt to renew the
146   * Kerberos TGT for an existing session.  It may be used in conjunction with
147   * the GSSAPI mechanism.
148   */
149  public static final String SASL_OPTION_RENEW_TGT = "renewTGT";
150
151
152
153  /**
154   * The name of the SASL option that specifies the path to the Kerberos ticket
155   * cache to use.  It may be used in conjunction with the GSSAPI mechanism.
156   */
157  public static final String SASL_OPTION_TICKET_CACHE_PATH = "ticketCache";
158
159
160
161  /**
162   * The name of the SASL option that specifies the trace string.  It may be
163   * used in conjunction with the ANONYMOUS mechanism.
164   */
165  public static final String SASL_OPTION_TRACE = "trace";
166
167
168
169  /**
170   * The name of the SASL option that specifies whether to use a Kerberos ticket
171   * cache.  It may be used in conjunction with the GSSAPI mechanism.
172   */
173  public static final String SASL_OPTION_USE_TICKET_CACHE = "useTicketCache";
174
175
176
177  /**
178   * A map with information about all supported SASL mechanisms, mapped from
179   * lowercase mechanism name to an object with information about that
180   * mechanism.
181   */
182  private static final Map<String,SASLMechanismInfo> SASL_MECHANISMS;
183
184
185
186  static
187  {
188    final TreeMap<String,SASLMechanismInfo> m =
189         new TreeMap<String,SASLMechanismInfo>();
190
191    m.put(toLowerCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME),
192         new SASLMechanismInfo(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME,
193              INFO_SASL_ANONYMOUS_DESCRIPTION.get(), false, false,
194              new SASLOption(SASL_OPTION_TRACE,
195                   INFO_SASL_ANONYMOUS_OPTION_TRACE.get(), false, false)));
196
197    m.put(toLowerCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME),
198         new SASLMechanismInfo(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME,
199              INFO_SASL_CRAM_MD5_DESCRIPTION.get(), true, true,
200              new SASLOption(SASL_OPTION_AUTH_ID,
201                   INFO_SASL_CRAM_MD5_OPTION_AUTH_ID.get(), true, false)));
202
203    m.put(toLowerCase(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME),
204         new SASLMechanismInfo(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME,
205              INFO_SASL_DIGEST_MD5_DESCRIPTION.get(), true, true,
206              new SASLOption(SASL_OPTION_AUTH_ID,
207                   INFO_SASL_DIGEST_MD5_OPTION_AUTH_ID.get(), true, false),
208              new SASLOption(SASL_OPTION_AUTHZ_ID,
209                   INFO_SASL_DIGEST_MD5_OPTION_AUTHZ_ID.get(), false, false),
210              new SASLOption(SASL_OPTION_REALM,
211                   INFO_SASL_DIGEST_MD5_OPTION_REALM.get(), false, false),
212              new SASLOption(SASL_OPTION_QOP,
213                   INFO_SASL_DIGEST_MD5_OPTION_QOP.get(), false, false)));
214
215    m.put(toLowerCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME),
216         new SASLMechanismInfo(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME,
217              INFO_SASL_EXTERNAL_DESCRIPTION.get(), false, false));
218
219    m.put(toLowerCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME),
220         new SASLMechanismInfo(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME,
221              INFO_SASL_GSSAPI_DESCRIPTION.get(), true, false,
222              new SASLOption(SASL_OPTION_AUTH_ID,
223                   INFO_SASL_GSSAPI_OPTION_AUTH_ID.get(), true, false),
224              new SASLOption(SASL_OPTION_AUTHZ_ID,
225                   INFO_SASL_GSSAPI_OPTION_AUTHZ_ID.get(), false, false),
226              new SASLOption(SASL_OPTION_CONFIG_FILE,
227                   INFO_SASL_GSSAPI_OPTION_CONFIG_FILE.get(), false, false),
228              new SASLOption(SASL_OPTION_DEBUG,
229                   INFO_SASL_GSSAPI_OPTION_DEBUG.get(), false, false),
230              new SASLOption(SASL_OPTION_KDC_ADDRESS,
231                   INFO_SASL_GSSAPI_OPTION_KDC_ADDRESS.get(), false, false),
232              new SASLOption(SASL_OPTION_PROTOCOL,
233                   INFO_SASL_GSSAPI_OPTION_PROTOCOL.get(), false, false),
234              new SASLOption(SASL_OPTION_REALM,
235                   INFO_SASL_GSSAPI_OPTION_REALM.get(), false, false),
236              new SASLOption(SASL_OPTION_QOP,
237                   INFO_SASL_GSSAPI_OPTION_QOP.get(), false, false),
238              new SASLOption(SASL_OPTION_RENEW_TGT,
239                   INFO_SASL_GSSAPI_OPTION_RENEW_TGT.get(), false, false),
240              new SASLOption(SASL_OPTION_REQUIRE_CACHE,
241                   INFO_SASL_GSSAPI_OPTION_REQUIRE_TICKET_CACHE.get(), false,
242                   false),
243              new SASLOption(SASL_OPTION_TICKET_CACHE_PATH,
244                   INFO_SASL_GSSAPI_OPTION_TICKET_CACHE.get(), false, false),
245              new SASLOption(SASL_OPTION_USE_TICKET_CACHE,
246                   INFO_SASL_GSSAPI_OPTION_USE_TICKET_CACHE.get(), false,
247                   false)));
248
249    m.put(toLowerCase(PLAINBindRequest.PLAIN_MECHANISM_NAME),
250         new SASLMechanismInfo(PLAINBindRequest.PLAIN_MECHANISM_NAME,
251              INFO_SASL_PLAIN_DESCRIPTION.get(), true, true,
252              new SASLOption(SASL_OPTION_AUTH_ID,
253                   INFO_SASL_PLAIN_OPTION_AUTH_ID.get(), true, false),
254              new SASLOption(SASL_OPTION_AUTHZ_ID,
255                   INFO_SASL_PLAIN_OPTION_AUTHZ_ID.get(), false, false)));
256
257
258    // If Commercial Edition classes are available, then register support for
259    // any additional SASL mechanisms that it provides.
260    try
261    {
262      final Class<?> c =
263           Class.forName("com.unboundid.ldap.sdk.unboundidds.SASLHelper");
264      final Method addCESASLInfoMethod =
265           c.getMethod("addCESASLInfo", Map.class);
266      addCESASLInfoMethod.invoke(null, m);
267    }
268    catch (final Exception e)
269    {
270        // This is fine.  It simply means that the Commercial Edition classes
271        // are not available.
272      Debug.debugException(e);
273    }
274
275    SASL_MECHANISMS = Collections.unmodifiableMap(m);
276  }
277
278
279
280  /**
281   * Prevent this utility class from being instantiated.
282   */
283  private SASLUtils()
284  {
285    // No implementation required.
286  }
287
288
289
290  /**
291   * Retrieves information about the SASL mechanisms supported for use by this
292   * class.
293   *
294   * @return  Information about the SASL mechanisms supported for use by this
295   *          class.
296   */
297  public static List<SASLMechanismInfo> getSupportedSASLMechanisms()
298  {
299    return Collections.unmodifiableList(new ArrayList<SASLMechanismInfo>(
300         SASL_MECHANISMS.values()));
301  }
302
303
304
305  /**
306   * Retrieves information about the specified SASL mechanism.
307   *
308   * @param  mechanism  The name of the SASL mechanism for which to retrieve
309   *                    information.  It will not be treated in a case-sensitive
310   *                    manner.
311   *
312   * @return  Information about the requested SASL mechanism, or {@code null} if
313   *          no information about the specified mechanism is available.
314   */
315  public static SASLMechanismInfo getSASLMechanismInfo(final String mechanism)
316  {
317    return SASL_MECHANISMS.get(toLowerCase(mechanism));
318  }
319
320
321
322  /**
323   * Creates a new SASL bind request using the provided information.
324   *
325   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
326   *                    SASL mechanisms, this should be {@code null}, since the
327   *                    identity of the target user should be specified in some
328   *                    other way (e.g., via an "authID" SASL option).
329   * @param  password   The password to use for the SASL bind request.  It may
330   *                    be {@code null} if no password is required for the
331   *                    desired SASL mechanism.
332   * @param  mechanism  The name of the SASL mechanism to use.  It may be
333   *                    {@code null} if the provided set of options contains a
334   *                    "mech" option to specify the desired SASL option.
335   * @param  options    The set of SASL options to use when creating the bind
336   *                    request, in the form "name=value".  It may be
337   *                    {@code null} or empty if no SASL options are needed and
338   *                    a value was provided for the {@code mechanism} argument.
339   *                    If the set of SASL options includes a "mech" option,
340   *                    then the {@code mechanism} argument must be {@code null}
341   *                    or have a value that matches the value of the "mech"
342   *                    SASL option.
343   *
344   * @return  The SASL bind request created using the provided information.
345   *
346   * @throws  LDAPException  If a problem is encountered while trying to create
347   *                         the SASL bind request.
348   */
349  public static SASLBindRequest createBindRequest(final String bindDN,
350                                                  final String password,
351                                                  final String mechanism,
352                                                  final String... options)
353         throws LDAPException
354  {
355    return createBindRequest(bindDN,
356         (password == null ? null : getBytes(password)), mechanism,
357         StaticUtils.toList(options));
358  }
359
360
361
362  /**
363   * Creates a new SASL bind request using the provided information.
364   *
365   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
366   *                    SASL mechanisms, this should be {@code null}, since the
367   *                    identity of the target user should be specified in some
368   *                    other way (e.g., via an "authID" SASL option).
369   * @param  password   The password to use for the SASL bind request.  It may
370   *                    be {@code null} if no password is required for the
371   *                    desired SASL mechanism.
372   * @param  mechanism  The name of the SASL mechanism to use.  It may be
373   *                    {@code null} if the provided set of options contains a
374   *                    "mech" option to specify the desired SASL option.
375   * @param  options    The set of SASL options to use when creating the bind
376   *                    request, in the form "name=value".  It may be
377   *                    {@code null} or empty if no SASL options are needed and
378   *                    a value was provided for the {@code mechanism} argument.
379   *                    If the set of SASL options includes a "mech" option,
380   *                    then the {@code mechanism} argument must be {@code null}
381   *                    or have a value that matches the value of the "mech"
382   *                    SASL option.
383   * @param  controls   The set of controls to include in the request.
384   *
385   * @return  The SASL bind request created using the provided information.
386   *
387   * @throws  LDAPException  If a problem is encountered while trying to create
388   *                         the SASL bind request.
389   */
390  public static SASLBindRequest createBindRequest(final String bindDN,
391                                                  final String password,
392                                                  final String mechanism,
393                                                  final List<String> options,
394                                                  final Control... controls)
395         throws LDAPException
396  {
397    return createBindRequest(bindDN,
398         (password == null ? null : getBytes(password)), mechanism, options,
399         controls);
400  }
401
402
403
404  /**
405   * Creates a new SASL bind request using the provided information.
406   *
407   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
408   *                    SASL mechanisms, this should be {@code null}, since the
409   *                    identity of the target user should be specified in some
410   *                    other way (e.g., via an "authID" SASL option).
411   * @param  password   The password to use for the SASL bind request.  It may
412   *                    be {@code null} if no password is required for the
413   *                    desired SASL mechanism.
414   * @param  mechanism  The name of the SASL mechanism to use.  It may be
415   *                    {@code null} if the provided set of options contains a
416   *                    "mech" option to specify the desired SASL option.
417   * @param  options    The set of SASL options to use when creating the bind
418   *                    request, in the form "name=value".  It may be
419   *                    {@code null} or empty if no SASL options are needed and
420   *                    a value was provided for the {@code mechanism} argument.
421   *                    If the set of SASL options includes a "mech" option,
422   *                    then the {@code mechanism} argument must be {@code null}
423   *                    or have a value that matches the value of the "mech"
424   *                    SASL option.
425   *
426   * @return  The SASL bind request created using the provided information.
427   *
428   * @throws  LDAPException  If a problem is encountered while trying to create
429   *                         the SASL bind request.
430   */
431  public static SASLBindRequest createBindRequest(final String bindDN,
432                                                  final byte[] password,
433                                                  final String mechanism,
434                                                  final String... options)
435         throws LDAPException
436  {
437    return createBindRequest(bindDN, password, mechanism,
438         StaticUtils.toList(options));
439  }
440
441
442
443  /**
444   * Creates a new SASL bind request using the provided information.
445   *
446   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
447   *                    SASL mechanisms, this should be {@code null}, since the
448   *                    identity of the target user should be specified in some
449   *                    other way (e.g., via an "authID" SASL option).
450   * @param  password   The password to use for the SASL bind request.  It may
451   *                    be {@code null} if no password is required for the
452   *                    desired SASL mechanism.
453   * @param  mechanism  The name of the SASL mechanism to use.  It may be
454   *                    {@code null} if the provided set of options contains a
455   *                    "mech" option to specify the desired SASL option.
456   * @param  options    The set of SASL options to use when creating the bind
457   *                    request, in the form "name=value".  It may be
458   *                    {@code null} or empty if no SASL options are needed and
459   *                    a value was provided for the {@code mechanism} argument.
460   *                    If the set of SASL options includes a "mech" option,
461   *                    then the {@code mechanism} argument must be {@code null}
462   *                    or have a value that matches the value of the "mech"
463   *                    SASL option.
464   * @param  controls   The set of controls to include in the request.
465   *
466   * @return  The SASL bind request created using the provided information.
467   *
468   * @throws  LDAPException  If a problem is encountered while trying to create
469   *                         the SASL bind request.
470   */
471  public static SASLBindRequest createBindRequest(final String bindDN,
472                                                  final byte[] password,
473                                                  final String mechanism,
474                                                  final List<String> options,
475                                                  final Control... controls)
476         throws LDAPException
477  {
478    return createBindRequest(bindDN, password, false, null, mechanism, options,
479         controls);
480  }
481
482
483
484  /**
485   * Creates a new SASL bind request using the provided information.
486   *
487   * @param  bindDN             The bind DN to use for the SASL bind request.
488   *                            For most SASL mechanisms, this should be
489   *                            {@code null}, since the identity of the target
490   *                            user should be specified in some other way
491   *                            (e.g., via an "authID" SASL option).
492   * @param  password           The password to use for the SASL bind request.
493   *                            It may be {@code null} if no password is
494   *                            required for the desired SASL mechanism.
495   * @param  promptForPassword  Indicates whether to interactively prompt for
496   *                            the password if one is needed but none was
497   *                            provided.
498   * @param  tool               The command-line tool whose input and output
499   *                            streams should be used when prompting for the
500   *                            bind password.  It may be {@code null} if
501   *                            {@code promptForPassword} is {@code false}.
502   * @param  mechanism          The name of the SASL mechanism to use.  It may
503   *                            be {@code null} if the provided set of options
504   *                            contains a "mech" option to specify the desired
505   *                            SASL option.
506   * @param  options            The set of SASL options to use when creating the
507   *                            bind request, in the form "name=value".  It may
508   *                            be {@code null} or empty if no SASL options are
509   *                            needed and a value was provided for the
510   *                            {@code mechanism} argument.  If the set of SASL
511   *                            options includes a "mech" option, then the
512   *                            {@code mechanism} argument must be {@code null}
513   *                            or have a value that matches the value of the
514   *                            "mech" SASL option.
515   * @param  controls           The set of controls to include in the request.
516   *
517   * @return  The SASL bind request created using the provided information.
518   *
519   * @throws  LDAPException  If a problem is encountered while trying to create
520   *                         the SASL bind request.
521   */
522  public static SASLBindRequest createBindRequest(final String bindDN,
523                                     final byte[] password,
524                                     final boolean promptForPassword,
525                                     final CommandLineTool tool,
526                                     final String mechanism,
527                                     final List<String> options,
528                                     final Control... controls)
529         throws LDAPException
530  {
531    if (promptForPassword)
532    {
533      Validator.ensureNotNull(tool);
534    }
535
536    // Parse the provided set of options to ensure that they are properly
537    // formatted in name-value form, and extract the SASL mechanism.
538    final String mech;
539    final Map<String,String> optionsMap = parseOptions(options);
540    final String mechOption =
541         optionsMap.remove(toLowerCase(SASL_OPTION_MECHANISM));
542    if (mechOption != null)
543    {
544      mech = mechOption;
545      if ((mechanism != null) && (! mech.equalsIgnoreCase(mechanism)))
546      {
547        throw new LDAPException(ResultCode.PARAM_ERROR,
548             ERR_SASL_OPTION_MECH_CONFLICT.get(mechanism, mech));
549      }
550    }
551    else
552    {
553      mech = mechanism;
554    }
555
556    if (mech == null)
557    {
558      throw new LDAPException(ResultCode.PARAM_ERROR,
559           ERR_SASL_OPTION_NO_MECH.get());
560    }
561
562    if (mech.equalsIgnoreCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME))
563    {
564      return createANONYMOUSBindRequest(password, optionsMap, controls);
565    }
566    else if (mech.equalsIgnoreCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME))
567    {
568      return createCRAMMD5BindRequest(password, promptForPassword, tool,
569           optionsMap, controls);
570    }
571    else if (mech.equalsIgnoreCase(
572                  DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME))
573    {
574      return createDIGESTMD5BindRequest(password, promptForPassword, tool,
575           optionsMap, controls);
576    }
577    else if (mech.equalsIgnoreCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME))
578    {
579      return createEXTERNALBindRequest(password, optionsMap, controls);
580    }
581    else if (mech.equalsIgnoreCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME))
582    {
583      return createGSSAPIBindRequest(password, promptForPassword, tool,
584           optionsMap, controls);
585    }
586    else if (mech.equalsIgnoreCase(PLAINBindRequest.PLAIN_MECHANISM_NAME))
587    {
588      return createPLAINBindRequest(password, promptForPassword, tool,
589           optionsMap, controls);
590    }
591    else
592    {
593      // If Commercial Edition classes are available, then see if the
594      // authentication attempt is for one of the Commercial Edition mechanisms.
595      try
596      {
597        final Class<?> c =
598             Class.forName("com.unboundid.ldap.sdk.unboundidds.SASLHelper");
599        final Method createBindRequestMethod = c.getMethod("createBindRequest",
600             String.class, StaticUtils.NO_BYTES.getClass(), String.class,
601             CommandLineTool.class, Map.class,
602             StaticUtils.NO_CONTROLS.getClass());
603        final Object bindRequestObject = createBindRequestMethod.invoke(null,
604             bindDN, password, mech, tool, optionsMap, controls);
605        if (bindRequestObject != null)
606        {
607          return (SASLBindRequest) bindRequestObject;
608        }
609      }
610      catch (final Exception e)
611      {
612        Debug.debugException(e);
613
614        // This may mean that there was a problem with the provided arguments.
615        // If it's an InvocationTargetException that wraps an LDAPException,
616        // then throw that LDAPException.
617        if (e instanceof InvocationTargetException)
618        {
619          final InvocationTargetException ite = (InvocationTargetException) e;
620          final Throwable t = ite.getTargetException();
621          if (t instanceof LDAPException)
622          {
623            throw (LDAPException) t;
624          }
625        }
626      }
627
628      throw new LDAPException(ResultCode.PARAM_ERROR,
629           ERR_SASL_OPTION_UNSUPPORTED_MECH.get(mech));
630    }
631  }
632
633
634
635  /**
636   * Creates a SASL ANONYMOUS bind request using the provided set of options.
637   *
638   * @param  password  The password to use for the bind request.
639   * @param  options   The set of SASL options for the bind request.
640   * @param  controls  The set of controls to include in the request.
641   *
642   * @return  The SASL ANONYMOUS bind request that was created.
643   *
644   * @throws  LDAPException  If a problem is encountered while trying to create
645   *                         the SASL bind request.
646   */
647  private static ANONYMOUSBindRequest createANONYMOUSBindRequest(
648                                           final byte[] password,
649                                           final Map<String,String> options,
650                                           final Control[] controls)
651          throws LDAPException
652  {
653    if (password != null)
654    {
655      throw new LDAPException(ResultCode.PARAM_ERROR,
656           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
657                ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME));
658    }
659
660
661    // The trace option is optional.
662    final String trace = options.remove(toLowerCase(SASL_OPTION_TRACE));
663
664    // Ensure no unsupported options were provided.
665    ensureNoUnsupportedOptions(options,
666         ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME);
667
668    return new ANONYMOUSBindRequest(trace, controls);
669  }
670
671
672
673  /**
674   * Creates a SASL CRAM-MD5 bind request using the provided password and set of
675   * options.
676   *
677   * @param  password           The password to use for the bind request.
678   * @param  promptForPassword  Indicates whether to interactively prompt for
679   *                            the password if one is needed but none was
680   *                            provided.
681   * @param  tool               The command-line tool whose input and output
682   *                            streams should be used when prompting for the
683   *                            bind password.  It may be {@code null} if
684   *                            {@code promptForPassword} is {@code false}.
685   * @param  options            The set of SASL options for the bind request.
686   * @param  controls           The set of controls to include in the request.
687   *
688   * @return  The SASL CRAM-MD5 bind request that was created.
689   *
690   * @throws  LDAPException  If a problem is encountered while trying to create
691   *                         the SASL bind request.
692   */
693  private static CRAMMD5BindRequest createCRAMMD5BindRequest(
694                                         final byte[] password,
695                                         final boolean promptForPassword,
696                                         final CommandLineTool tool,
697                                         final Map<String,String> options,
698                                         final Control[] controls)
699          throws LDAPException
700  {
701    final byte[] pw;
702    if (password == null)
703    {
704      if (promptForPassword)
705      {
706        tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
707        pw = PasswordReader.readPassword();
708        tool.getOriginalOut().println();
709      }
710      else
711      {
712        throw new LDAPException(ResultCode.PARAM_ERROR,
713             ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
714                  CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
715      }
716    }
717    else
718    {
719      pw = password;
720    }
721
722
723    // The authID option is required.
724    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
725    if (authID == null)
726    {
727      throw new LDAPException(ResultCode.PARAM_ERROR,
728           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
729                CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
730    }
731
732
733    // Ensure no unsupported options were provided.
734    ensureNoUnsupportedOptions(options,
735         CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME);
736
737    return new CRAMMD5BindRequest(authID, pw, controls);
738  }
739
740
741
742  /**
743   * Creates a SASL DIGEST-MD5 bind request using the provided password and set
744   * of options.
745   *
746   * @param  password           The password to use for the bind request.
747   * @param  promptForPassword  Indicates whether to interactively prompt for
748   *                            the password if one is needed but none was
749   *                            provided.
750   * @param  tool               The command-line tool whose input and output
751   *                            streams should be used when prompting for the
752   *                            bind password.  It may be {@code null} if
753   *                            {@code promptForPassword} is {@code false}.
754   * @param  options            The set of SASL options for the bind request.
755   * @param  controls           The set of controls to include in the request.
756   *
757   * @return  The SASL DIGEST-MD5 bind request that was created.
758   *
759   * @throws  LDAPException  If a problem is encountered while trying to create
760   *                         the SASL bind request.
761   */
762  private static DIGESTMD5BindRequest createDIGESTMD5BindRequest(
763                                           final byte[] password,
764                                           final boolean promptForPassword,
765                                           final CommandLineTool tool,
766                                           final Map<String,String> options,
767                                           final Control[] controls)
768          throws LDAPException
769  {
770    final byte[] pw;
771    if (password == null)
772    {
773      if (promptForPassword)
774      {
775        tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
776        pw = PasswordReader.readPassword();
777        tool.getOriginalOut().println();
778      }
779      else
780      {
781        throw new LDAPException(ResultCode.PARAM_ERROR,
782             ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
783                  CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
784      }
785    }
786    else
787    {
788      pw = password;
789    }
790
791    // The authID option is required.
792    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
793    if (authID == null)
794    {
795      throw new LDAPException(ResultCode.PARAM_ERROR,
796           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
797                CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
798    }
799
800    final DIGESTMD5BindRequestProperties properties =
801         new DIGESTMD5BindRequestProperties(authID, pw);
802
803    // The authzID option is optional.
804    properties.setAuthorizationID(
805         options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID)));
806
807    // The realm option is optional.
808    properties.setRealm(options.remove(toLowerCase(SASL_OPTION_REALM)));
809
810    // The QoP option is optional, and may contain multiple values that need to
811    // be parsed.
812    final String qopString = options.remove(toLowerCase(SASL_OPTION_QOP));
813    if (qopString != null)
814    {
815      properties.setAllowedQoP(
816           SASLQualityOfProtection.decodeQoPList(qopString));
817    }
818
819    // Ensure no unsupported options were provided.
820    ensureNoUnsupportedOptions(options,
821         DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME);
822
823    return new DIGESTMD5BindRequest(properties, controls);
824  }
825
826
827
828  /**
829   * Creates a SASL EXTERNAL bind request using the provided set of options.
830   *
831   * @param  password  The password to use for the bind request.
832   * @param  options   The set of SASL options for the bind request.
833   * @param  controls  The set of controls to include in the request.
834   *
835   * @return  The SASL EXTERNAL bind request that was created.
836   *
837   * @throws  LDAPException  If a problem is encountered while trying to create
838   *                         the SASL bind request.
839   */
840  private static EXTERNALBindRequest createEXTERNALBindRequest(
841                                          final byte[] password,
842                                          final Map<String,String> options,
843                                          final Control[] controls)
844          throws LDAPException
845  {
846    if (password != null)
847    {
848      throw new LDAPException(ResultCode.PARAM_ERROR,
849           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
850                EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME));
851    }
852
853    // Ensure no unsupported options were provided.
854    ensureNoUnsupportedOptions(options,
855         EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME);
856
857    return new EXTERNALBindRequest(controls);
858  }
859
860
861
862  /**
863   * Creates a SASL GSSAPI bind request using the provided password and set of
864   * options.
865   *
866   * @param  password           The password to use for the bind request.
867   * @param  promptForPassword  Indicates whether to interactively prompt for
868   *                            the password if one is needed but none was
869   *                            provided.
870   * @param  tool               The command-line tool whose input and output
871   *                            streams should be used when prompting for the
872   *                            bind password.  It may be {@code null} if
873   *                            {@code promptForPassword} is {@code false}.
874   * @param  options            The set of SASL options for the bind request.
875   * @param  controls           The set of controls to include in the request.
876   *
877   * @return  The SASL GSSAPI bind request that was created.
878   *
879   * @throws  LDAPException  If a problem is encountered while trying to create
880   *                         the SASL bind request.
881   */
882  private static GSSAPIBindRequest createGSSAPIBindRequest(
883                                        final byte[] password,
884                                        final boolean promptForPassword,
885                                        final CommandLineTool tool,
886                                        final Map<String,String> options,
887                                        final Control[] controls)
888          throws LDAPException
889  {
890    // The authID option is required.
891    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
892    if (authID == null)
893    {
894      throw new LDAPException(ResultCode.PARAM_ERROR,
895           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
896                GSSAPIBindRequest.GSSAPI_MECHANISM_NAME));
897    }
898    final GSSAPIBindRequestProperties gssapiProperties =
899         new GSSAPIBindRequestProperties(authID, password);
900
901    // The authzID option is optional.
902    gssapiProperties.setAuthorizationID(
903         options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID)));
904
905    // The configFile option is optional.
906    gssapiProperties.setConfigFilePath(options.remove(toLowerCase(
907         SASL_OPTION_CONFIG_FILE)));
908
909    // The debug option is optional.
910    gssapiProperties.setEnableGSSAPIDebugging(getBooleanValue(options,
911         SASL_OPTION_DEBUG, false));
912
913    // The kdcAddress option is optional.
914    gssapiProperties.setKDCAddress(options.remove(
915         toLowerCase(SASL_OPTION_KDC_ADDRESS)));
916
917    // The protocol option is optional.
918    final String protocol = options.remove(toLowerCase(SASL_OPTION_PROTOCOL));
919    if (protocol != null)
920    {
921      gssapiProperties.setServicePrincipalProtocol(protocol);
922    }
923
924    // The realm option is optional.
925    gssapiProperties.setRealm(options.remove(toLowerCase(SASL_OPTION_REALM)));
926
927    // The QoP option is optional, and may contain multiple values that need to
928    // be parsed.
929    final String qopString = options.remove(toLowerCase(SASL_OPTION_QOP));
930    if (qopString != null)
931    {
932      gssapiProperties.setAllowedQoP(
933           SASLQualityOfProtection.decodeQoPList(qopString));
934    }
935
936    // The renewTGT option is optional.
937    gssapiProperties.setRenewTGT(getBooleanValue(options, SASL_OPTION_RENEW_TGT,
938         false));
939
940    // The requireCache option is optional.
941    gssapiProperties.setRequireCachedCredentials(getBooleanValue(options,
942         SASL_OPTION_REQUIRE_CACHE, false));
943
944    // The ticketCache option is optional.
945    gssapiProperties.setTicketCachePath(options.remove(toLowerCase(
946         SASL_OPTION_TICKET_CACHE_PATH)));
947
948    // The useTicketCache option is optional.
949    gssapiProperties.setUseTicketCache(getBooleanValue(options,
950         SASL_OPTION_USE_TICKET_CACHE, true));
951
952    // Ensure no unsupported options were provided.
953    ensureNoUnsupportedOptions(options,
954         GSSAPIBindRequest.GSSAPI_MECHANISM_NAME);
955
956    // A password must have been provided unless useTicketCache=true and
957    // requireTicketCache=true.
958    if (password == null)
959    {
960      if (! (gssapiProperties.useTicketCache() &&
961           gssapiProperties.requireCachedCredentials()))
962      {
963        if (promptForPassword)
964        {
965          tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
966          gssapiProperties.setPassword(PasswordReader.readPassword());
967          tool.getOriginalOut().println();
968        }
969        else
970        {
971          throw new LDAPException(ResultCode.PARAM_ERROR,
972               ERR_SASL_OPTION_GSSAPI_PASSWORD_REQUIRED.get());
973        }
974      }
975    }
976
977    return new GSSAPIBindRequest(gssapiProperties, controls);
978  }
979
980
981
982  /**
983   * Creates a SASL PLAIN bind request using the provided password and set of
984   * options.
985   *
986   * @param  password           The password to use for the bind request.
987   * @param  promptForPassword  Indicates whether to interactively prompt for
988   *                            the password if one is needed but none was
989   *                            provided.
990   * @param  tool               The command-line tool whose input and output
991   *                            streams should be used when prompting for the
992   *                            bind password.  It may be {@code null} if
993   *                            {@code promptForPassword} is {@code false}.
994   * @param  options            The set of SASL options for the bind request.
995   * @param  controls           The set of controls to include in the request.
996   *
997   * @return  The SASL PLAIN bind request that was created.
998   *
999   * @throws  LDAPException  If a problem is encountered while trying to create
1000   *                         the SASL bind request.
1001   */
1002  private static PLAINBindRequest createPLAINBindRequest(
1003                                        final byte[] password,
1004                                        final boolean promptForPassword,
1005                                        final CommandLineTool tool,
1006                                        final Map<String,String> options,
1007                                        final Control[] controls)
1008          throws LDAPException
1009  {
1010    final byte[] pw;
1011    if (password == null)
1012    {
1013      if (promptForPassword)
1014      {
1015        tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
1016        pw = PasswordReader.readPassword();
1017        tool.getOriginalOut().println();
1018      }
1019      else
1020      {
1021        throw new LDAPException(ResultCode.PARAM_ERROR,
1022             ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
1023                  CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
1024      }
1025    }
1026    else
1027    {
1028      pw = password;
1029    }
1030
1031    // The authID option is required.
1032    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
1033    if (authID == null)
1034    {
1035      throw new LDAPException(ResultCode.PARAM_ERROR,
1036           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
1037                PLAINBindRequest.PLAIN_MECHANISM_NAME));
1038    }
1039
1040    // The authzID option is optional.
1041    final String authzID = options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID));
1042
1043    // Ensure no unsupported options were provided.
1044    ensureNoUnsupportedOptions(options,
1045         PLAINBindRequest.PLAIN_MECHANISM_NAME);
1046
1047    return new PLAINBindRequest(authID, authzID, pw, controls);
1048  }
1049
1050
1051
1052  /**
1053   * Parses the provided list of SASL options.
1054   *
1055   * @param  options  The list of options to be parsed.
1056   *
1057   * @return  A map with the parsed set of options.
1058   *
1059   * @throws  LDAPException  If a problem is encountered while parsing options.
1060   */
1061  private static Map<String,String>
1062                      parseOptions(final List<String> options)
1063          throws LDAPException
1064  {
1065    if (options == null)
1066    {
1067      return new HashMap<String,String>(0);
1068    }
1069
1070    final HashMap<String,String> m = new HashMap<String,String>(options.size());
1071    for (final String s : options)
1072    {
1073      final int equalPos = s.indexOf('=');
1074      if (equalPos < 0)
1075      {
1076        throw new LDAPException(ResultCode.PARAM_ERROR,
1077             ERR_SASL_OPTION_MISSING_EQUAL.get(s));
1078      }
1079      else if (equalPos == 0)
1080      {
1081        throw new LDAPException(ResultCode.PARAM_ERROR,
1082             ERR_SASL_OPTION_STARTS_WITH_EQUAL.get(s));
1083      }
1084
1085      final String name = s.substring(0, equalPos);
1086      final String value = s.substring(equalPos + 1);
1087      if (m.put(toLowerCase(name), value) != null)
1088      {
1089        throw new LDAPException(ResultCode.PARAM_ERROR,
1090             ERR_SASL_OPTION_NOT_MULTI_VALUED.get(name));
1091      }
1092    }
1093
1094    return m;
1095  }
1096
1097
1098
1099  /**
1100   * Ensures that the provided map is empty, and will throw an exception if it
1101   * isn't.  This method is intended for internal use only.
1102   *
1103   * @param  options    The map of options to ensure is empty.
1104   * @param  mechanism  The associated SASL mechanism.
1105   *
1106   * @throws  LDAPException  If the map of SASL options is not empty.
1107   */
1108  @InternalUseOnly()
1109  public static void ensureNoUnsupportedOptions(
1110                          final Map<String,String> options,
1111                          final String mechanism)
1112          throws LDAPException
1113  {
1114    if (! options.isEmpty())
1115    {
1116      for (final String s : options.keySet())
1117      {
1118        throw new LDAPException(ResultCode.PARAM_ERROR,
1119             ERR_SASL_OPTION_UNSUPPORTED_FOR_MECH.get(s,mechanism));
1120      }
1121    }
1122  }
1123
1124
1125
1126  /**
1127   * Retrieves the value of the specified option and parses it as a boolean.
1128   * Values of "true", "t", "yes", "y", "on", and "1" will be treated as
1129   * {@code true}.  Values of "false", "f", "no", "n", "off", and "0" will be
1130   * treated as {@code false}.
1131   *
1132   * @param  m  The map from which to retrieve the option.  It must not be
1133   *            {@code null}.
1134   * @param  o  The name of the option to examine.
1135   * @param  d  The default value to use if the given option was not provided.
1136   *
1137   * @return  The parsed boolean value.
1138   *
1139   * @throws  LDAPException  If the option value cannot be parsed as a boolean.
1140   */
1141  static boolean getBooleanValue(final Map<String,String> m, final String o,
1142                                 final boolean d)
1143         throws LDAPException
1144  {
1145    final String s = toLowerCase(m.remove(toLowerCase(o)));
1146    if (s == null)
1147    {
1148      return d;
1149    }
1150    else if (s.equals("true") ||
1151             s.equals("t") ||
1152             s.equals("yes") ||
1153             s.equals("y") ||
1154             s.equals("on") ||
1155             s.equals("1"))
1156    {
1157      return true;
1158    }
1159    else if (s.equals("false") ||
1160             s.equals("f") ||
1161             s.equals("no") ||
1162             s.equals("n") ||
1163             s.equals("off") ||
1164             s.equals("0"))
1165    {
1166      return false;
1167    }
1168    else
1169    {
1170      throw new LDAPException(ResultCode.PARAM_ERROR,
1171           ERR_SASL_OPTION_MALFORMED_BOOLEAN_VALUE.get(o));
1172    }
1173  }
1174
1175
1176
1177  /**
1178   * Retrieves a string representation of the SASL usage information.  This will
1179   * include the supported SASL mechanisms and the properties that may be used
1180   * with each.
1181   *
1182   * @param  maxWidth  The maximum line width to use for the output.  If this is
1183   *                   less than or equal to zero, then no wrapping will be
1184   *                   performed.
1185   *
1186   * @return  A string representation of the usage information
1187   */
1188  public static String getUsageString(final int maxWidth)
1189  {
1190    final StringBuilder buffer = new StringBuilder();
1191
1192    for (final String line : getUsage(maxWidth))
1193    {
1194      buffer.append(line);
1195      buffer.append(EOL);
1196    }
1197
1198    return buffer.toString();
1199  }
1200
1201
1202
1203  /**
1204   * Retrieves lines that make up the SASL usage information, optionally
1205   * wrapping long lines.
1206   *
1207   * @param  maxWidth  The maximum line width to use for the output.  If this is
1208   *                   less than or equal to zero, then no wrapping will be
1209   *                   performed.
1210   *
1211   * @return  The lines that make up the SASL usage information.
1212   */
1213  public static List<String> getUsage(final int maxWidth)
1214  {
1215    final ArrayList<String> lines = new ArrayList<String>(100);
1216
1217    boolean first = true;
1218    for (final SASLMechanismInfo i : getSupportedSASLMechanisms())
1219    {
1220      if (first)
1221      {
1222        first = false;
1223      }
1224      else
1225      {
1226        lines.add("");
1227        lines.add("");
1228      }
1229
1230      lines.addAll(
1231           wrapLine(INFO_SASL_HELP_MECHANISM.get(i.getName()), maxWidth));
1232      lines.add("");
1233
1234      for (final String line : wrapLine(i.getDescription(), maxWidth - 4))
1235      {
1236        lines.add("  " + line);
1237      }
1238      lines.add("");
1239
1240      for (final String line :
1241           wrapLine(INFO_SASL_HELP_MECHANISM_OPTIONS.get(i.getName()),
1242                maxWidth - 4))
1243      {
1244        lines.add("  " + line);
1245      }
1246
1247      if (i.acceptsPassword())
1248      {
1249        lines.add("");
1250        if (i.requiresPassword())
1251        {
1252          for (final String line :
1253               wrapLine(INFO_SASL_HELP_PASSWORD_REQUIRED.get(i.getName()),
1254                    maxWidth - 4))
1255          {
1256            lines.add("  " + line);
1257          }
1258        }
1259        else
1260        {
1261          for (final String line :
1262               wrapLine(INFO_SASL_HELP_PASSWORD_OPTIONAL.get(i.getName()),
1263                    maxWidth - 4))
1264          {
1265            lines.add("  " + line);
1266          }
1267        }
1268      }
1269
1270      for (final SASLOption o : i.getOptions())
1271      {
1272        lines.add("");
1273        lines.add("  * " + o.getName());
1274        for (final String line : wrapLine(o.getDescription(), maxWidth - 14))
1275        {
1276          lines.add("       " + line);
1277        }
1278      }
1279    }
1280
1281    return lines;
1282  }
1283}