001    /*
002     * Copyright 2011-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2011-2016 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     */
021    package com.unboundid.util;
022    
023    
024    
025    import java.lang.reflect.InvocationTargetException;
026    import java.lang.reflect.Method;
027    import java.util.ArrayList;
028    import java.util.Collections;
029    import java.util.HashMap;
030    import java.util.List;
031    import java.util.Map;
032    import java.util.TreeMap;
033    
034    import com.unboundid.ldap.sdk.ANONYMOUSBindRequest;
035    import com.unboundid.ldap.sdk.Control;
036    import com.unboundid.ldap.sdk.CRAMMD5BindRequest;
037    import com.unboundid.ldap.sdk.DIGESTMD5BindRequest;
038    import com.unboundid.ldap.sdk.DIGESTMD5BindRequestProperties;
039    import com.unboundid.ldap.sdk.EXTERNALBindRequest;
040    import com.unboundid.ldap.sdk.GSSAPIBindRequest;
041    import com.unboundid.ldap.sdk.GSSAPIBindRequestProperties;
042    import com.unboundid.ldap.sdk.LDAPException;
043    import com.unboundid.ldap.sdk.PLAINBindRequest;
044    import com.unboundid.ldap.sdk.ResultCode;
045    import com.unboundid.ldap.sdk.SASLBindRequest;
046    import com.unboundid.ldap.sdk.SASLQualityOfProtection;
047    
048    import static com.unboundid.util.StaticUtils.*;
049    import 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)
058    public 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    }