001    /*
002     * Copyright 2011-2013 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2011-2013 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.EXTERNALBindRequest;
039    import com.unboundid.ldap.sdk.GSSAPIBindRequest;
040    import com.unboundid.ldap.sdk.GSSAPIBindRequestProperties;
041    import com.unboundid.ldap.sdk.LDAPException;
042    import com.unboundid.ldap.sdk.PLAINBindRequest;
043    import com.unboundid.ldap.sdk.ResultCode;
044    import com.unboundid.ldap.sdk.SASLBindRequest;
045    
046    import static com.unboundid.util.StaticUtils.*;
047    import static com.unboundid.util.UtilityMessages.*;
048    
049    
050    
051    /**
052     * This class provides a utility that may be used to help process SASL bind
053     * operations using the LDAP SDK.
054     */
055    @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
056    public final class SASLUtils
057    {
058      /**
059       * The name of the SASL option that specifies the authentication ID.  It may
060       * be used in conjunction with the CRAM-MD5, DIGEST-MD5, GSSAPI, and PLAIN
061       * mechanisms.
062       */
063      public static final String SASL_OPTION_AUTH_ID = "authID";
064    
065    
066    
067      /**
068       * The name of the SASL option that specifies the authorization ID.  It may
069       * be used in conjunction with the DIGEST-MD5, GSSAPI, and PLAIN mechanisms.
070       */
071      public static final String SASL_OPTION_AUTHZ_ID = "authzID";
072    
073    
074    
075      /**
076       * The name of the SASL option that specifies the path to the JAAS config
077       * file.  It may be used in conjunction with the GSSAPI mechanism.
078       */
079      public static final String SASL_OPTION_CONFIG_FILE = "configFile";
080    
081    
082    
083      /**
084       * The name of the SASL option that indicates whether debugging should be
085       * enabled.  It may be used in conjunction with the GSSAPI mechanism.
086       */
087      public static final String SASL_OPTION_DEBUG = "debug";
088    
089    
090    
091      /**
092       * The name of the SASL option that specifies the KDC address.  It may be used
093       * in conjunction with the GSSAPI mechanism.
094       */
095      public static final String SASL_OPTION_KDC_ADDRESS = "kdcAddress";
096    
097    
098    
099    
100      /**
101       * The name of the SASL option that specifies the desired SASL mechanism to
102       * use to authenticate to the server.
103       */
104      public static final String SASL_OPTION_MECHANISM = "mech";
105    
106    
107    
108      /**
109       * The name of the SASL option that specifies the GSSAPI service principal
110       * protocol.  It may be used in conjunction with the GSSAPI mechanism.
111       */
112      public static final String SASL_OPTION_PROTOCOL = "protocol";
113    
114    
115    
116      /**
117       * The name of the SASL option that specifies the realm name.  It may be used
118       * in conjunction with the DIGEST-MD5 and GSSAPI mechanisms.
119       */
120      public static final String SASL_OPTION_REALM = "realm";
121    
122    
123    
124      /**
125       * The name of the SASL option that indicates whether to require an existing
126       * Kerberos session from the ticket cache.  It may be used in conjunction with
127       * the GSSAPI mechanism.
128       */
129      public static final String SASL_OPTION_REQUIRE_CACHE = "requireCache";
130    
131    
132    
133      /**
134       * The name of the SASL option that indicates whether to attempt to renew the
135       * Kerberos TGT for an existing session.  It may be used in conjunction with
136       * the GSSAPI mechanism.
137       */
138      public static final String SASL_OPTION_RENEW_TGT = "renewTGT";
139    
140    
141    
142      /**
143       * The name of the SASL option that specifies the path to the Kerberos ticket
144       * cache to use.  It may be used in conjunction with the GSSAPI mechanism.
145       */
146      public static final String SASL_OPTION_TICKET_CACHE_PATH = "ticketCache";
147    
148    
149    
150      /**
151       * The name of the SASL option that specifies the trace string.  It may be
152       * used in conjunction with the ANONYMOUS mechanism.
153       */
154      public static final String SASL_OPTION_TRACE = "trace";
155    
156    
157    
158      /**
159       * The name of the SASL option that specifies whether to use a Kerberos ticket
160       * cache.  It may be used in conjunction with the GSSAPI mechanism.
161       */
162      public static final String SASL_OPTION_USE_TICKET_CACHE = "useTicketCache";
163    
164    
165    
166      /**
167       * A map with information about all supported SASL mechanisms, mapped from
168       * lowercase mechanism name to an object with information about that
169       * mechanism.
170       */
171      private static final Map<String,SASLMechanismInfo> SASL_MECHANISMS;
172    
173    
174    
175      static
176      {
177        final TreeMap<String,SASLMechanismInfo> m =
178             new TreeMap<String,SASLMechanismInfo>();
179    
180        m.put(toLowerCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME),
181             new SASLMechanismInfo(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME,
182                  INFO_SASL_ANONYMOUS_DESCRIPTION.get(), false, false,
183                  new SASLOption(SASL_OPTION_TRACE,
184                       INFO_SASL_ANONYMOUS_OPTION_TRACE.get(), false, false)));
185    
186        m.put(toLowerCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME),
187             new SASLMechanismInfo(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME,
188                  INFO_SASL_CRAM_MD5_DESCRIPTION.get(), true, true,
189                  new SASLOption(SASL_OPTION_AUTH_ID,
190                       INFO_SASL_CRAM_MD5_OPTION_AUTH_ID.get(), true, false)));
191    
192        m.put(toLowerCase(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME),
193             new SASLMechanismInfo(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME,
194                  INFO_SASL_DIGEST_MD5_DESCRIPTION.get(), true, true,
195                  new SASLOption(SASL_OPTION_AUTH_ID,
196                       INFO_SASL_DIGEST_MD5_OPTION_AUTH_ID.get(), true, false),
197                  new SASLOption(SASL_OPTION_AUTHZ_ID,
198                       INFO_SASL_DIGEST_MD5_OPTION_AUTHZ_ID.get(), false, false),
199                  new SASLOption(SASL_OPTION_REALM,
200                       INFO_SASL_DIGEST_MD5_OPTION_REALM.get(), false, false)));
201    
202        m.put(toLowerCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME),
203             new SASLMechanismInfo(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME,
204                  INFO_SASL_EXTERNAL_DESCRIPTION.get(), false, false));
205    
206        m.put(toLowerCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME),
207             new SASLMechanismInfo(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME,
208                  INFO_SASL_GSSAPI_DESCRIPTION.get(), true, false,
209                  new SASLOption(SASL_OPTION_AUTH_ID,
210                       INFO_SASL_GSSAPI_OPTION_AUTH_ID.get(), true, false),
211                  new SASLOption(SASL_OPTION_AUTHZ_ID,
212                       INFO_SASL_GSSAPI_OPTION_AUTHZ_ID.get(), false, false),
213                  new SASLOption(SASL_OPTION_CONFIG_FILE,
214                       INFO_SASL_GSSAPI_OPTION_CONFIG_FILE.get(), false, false),
215                  new SASLOption(SASL_OPTION_DEBUG,
216                       INFO_SASL_GSSAPI_OPTION_DEBUG.get(), false, false),
217                  new SASLOption(SASL_OPTION_KDC_ADDRESS,
218                       INFO_SASL_GSSAPI_OPTION_KDC_ADDRESS.get(), false, false),
219                  new SASLOption(SASL_OPTION_PROTOCOL,
220                       INFO_SASL_GSSAPI_OPTION_PROTOCOL.get(), false, false),
221                  new SASLOption(SASL_OPTION_REALM,
222                       INFO_SASL_GSSAPI_OPTION_REALM.get(), false, false),
223                  new SASLOption(SASL_OPTION_RENEW_TGT,
224                       INFO_SASL_GSSAPI_OPTION_RENEW_TGT.get(), false, false),
225                  new SASLOption(SASL_OPTION_REQUIRE_CACHE,
226                       INFO_SASL_GSSAPI_OPTION_REQUIRE_TICKET_CACHE.get(), false,
227                       false),
228                  new SASLOption(SASL_OPTION_TICKET_CACHE_PATH,
229                       INFO_SASL_GSSAPI_OPTION_TICKET_CACHE.get(), false, false),
230                  new SASLOption(SASL_OPTION_USE_TICKET_CACHE,
231                       INFO_SASL_GSSAPI_OPTION_USE_TICKET_CACHE.get(), false,
232                       false)));
233    
234        m.put(toLowerCase(PLAINBindRequest.PLAIN_MECHANISM_NAME),
235             new SASLMechanismInfo(PLAINBindRequest.PLAIN_MECHANISM_NAME,
236                  INFO_SASL_PLAIN_DESCRIPTION.get(), true, true,
237                  new SASLOption(SASL_OPTION_AUTH_ID,
238                       INFO_SASL_PLAIN_OPTION_AUTH_ID.get(), true, false),
239                  new SASLOption(SASL_OPTION_AUTHZ_ID,
240                       INFO_SASL_PLAIN_OPTION_AUTHZ_ID.get(), false, false)));
241    
242    
243        // If Commercial Edition classes are available, then register support for
244        // any additional SASL mechanisms that it provides.
245        try
246        {
247          final Class<?> c =
248               Class.forName("com.unboundid.ldap.sdk.unboundidds.SASLHelper");
249          final Method addCESASLInfoMethod =
250               c.getMethod("addCESASLInfo", Map.class);
251          addCESASLInfoMethod.invoke(null, m);
252        }
253        catch (final Exception e)
254        {
255            // This is fine.  It simply means that the Commercial Edition classes
256            // are not available.
257          Debug.debugException(e);
258        }
259    
260        SASL_MECHANISMS = Collections.unmodifiableMap(m);
261      }
262    
263    
264    
265      /**
266       * Prevent this utility class from being instantiated.
267       */
268      private SASLUtils()
269      {
270        // No implementation required.
271      }
272    
273    
274    
275      /**
276       * Retrieves information about the SASL mechanisms supported for use by this
277       * class.
278       *
279       * @return  Information about the SASL mechanisms supported for use by this
280       *          class.
281       */
282      public static List<SASLMechanismInfo> getSupportedSASLMechanisms()
283      {
284        return Collections.unmodifiableList(new ArrayList<SASLMechanismInfo>(
285             SASL_MECHANISMS.values()));
286      }
287    
288    
289    
290      /**
291       * Retrieves information about the specified SASL mechanism.
292       *
293       * @param  mechanism  The name of the SASL mechanism for which to retrieve
294       *                    information.  It will not be treated in a case-sensitive
295       *                    manner.
296       *
297       * @return  Information about the requested SASL mechanism, or {@code null} if
298       *          no information about the specified mechanism is available.
299       */
300      public static SASLMechanismInfo getSASLMechanismInfo(final String mechanism)
301      {
302        return SASL_MECHANISMS.get(toLowerCase(mechanism));
303      }
304    
305    
306    
307      /**
308       * Creates a new SASL bind request using the provided information.
309       *
310       * @param  bindDN     The bind DN to use for the SASL bind request.  For most
311       *                    SASL mechanisms, this should be {@code null}, since the
312       *                    identity of the target user should be specified in some
313       *                    other way (e.g., via an "authID" SASL option).
314       * @param  password   The password to use for the SASL bind request.  It may
315       *                    be {@code null} if no password is required for the
316       *                    desired SASL mechanism.
317       * @param  mechanism  The name of the SASL mechanism to use.  It may be
318       *                    {@code null} if the provided set of options contains a
319       *                    "mech" option to specify the desired SASL option.
320       * @param  options    The set of SASL options to use when creating the bind
321       *                    request, in the form "name=value".  It may be
322       *                    {@code null} or empty if no SASL options are needed and
323       *                    a value was provided for the {@code mechanism} argument.
324       *                    If the set of SASL options includes a "mech" option,
325       *                    then the {@code mechanism} argument must be {@code null}
326       *                    or have a value that matches the value of the "mech"
327       *                    SASL option.
328       *
329       * @return  The SASL bind request created using the provided information.
330       *
331       * @throws  LDAPException  If a problem is encountered while trying to create
332       *                         the SASL bind request.
333       */
334      public static SASLBindRequest createBindRequest(final String bindDN,
335                                                      final String password,
336                                                      final String mechanism,
337                                                      final String... options)
338             throws LDAPException
339      {
340        return createBindRequest(bindDN,
341             (password == null ? null : getBytes(password)), mechanism,
342             StaticUtils.toList(options));
343      }
344    
345    
346    
347      /**
348       * Creates a new SASL bind request using the provided information.
349       *
350       * @param  bindDN     The bind DN to use for the SASL bind request.  For most
351       *                    SASL mechanisms, this should be {@code null}, since the
352       *                    identity of the target user should be specified in some
353       *                    other way (e.g., via an "authID" SASL option).
354       * @param  password   The password to use for the SASL bind request.  It may
355       *                    be {@code null} if no password is required for the
356       *                    desired SASL mechanism.
357       * @param  mechanism  The name of the SASL mechanism to use.  It may be
358       *                    {@code null} if the provided set of options contains a
359       *                    "mech" option to specify the desired SASL option.
360       * @param  options    The set of SASL options to use when creating the bind
361       *                    request, in the form "name=value".  It may be
362       *                    {@code null} or empty if no SASL options are needed and
363       *                    a value was provided for the {@code mechanism} argument.
364       *                    If the set of SASL options includes a "mech" option,
365       *                    then the {@code mechanism} argument must be {@code null}
366       *                    or have a value that matches the value of the "mech"
367       *                    SASL option.
368       * @param  controls   The set of controls to include in the request.
369       *
370       * @return  The SASL bind request created using the provided information.
371       *
372       * @throws  LDAPException  If a problem is encountered while trying to create
373       *                         the SASL bind request.
374       */
375      public static SASLBindRequest createBindRequest(final String bindDN,
376                                                      final String password,
377                                                      final String mechanism,
378                                                      final List<String> options,
379                                                      final Control... controls)
380             throws LDAPException
381      {
382        return createBindRequest(bindDN,
383             (password == null ? null : getBytes(password)), mechanism, options,
384             controls);
385      }
386    
387    
388    
389      /**
390       * Creates a new SASL bind request using the provided information.
391       *
392       * @param  bindDN     The bind DN to use for the SASL bind request.  For most
393       *                    SASL mechanisms, this should be {@code null}, since the
394       *                    identity of the target user should be specified in some
395       *                    other way (e.g., via an "authID" SASL option).
396       * @param  password   The password to use for the SASL bind request.  It may
397       *                    be {@code null} if no password is required for the
398       *                    desired SASL mechanism.
399       * @param  mechanism  The name of the SASL mechanism to use.  It may be
400       *                    {@code null} if the provided set of options contains a
401       *                    "mech" option to specify the desired SASL option.
402       * @param  options    The set of SASL options to use when creating the bind
403       *                    request, in the form "name=value".  It may be
404       *                    {@code null} or empty if no SASL options are needed and
405       *                    a value was provided for the {@code mechanism} argument.
406       *                    If the set of SASL options includes a "mech" option,
407       *                    then the {@code mechanism} argument must be {@code null}
408       *                    or have a value that matches the value of the "mech"
409       *                    SASL option.
410       *
411       * @return  The SASL bind request created using the provided information.
412       *
413       * @throws  LDAPException  If a problem is encountered while trying to create
414       *                         the SASL bind request.
415       */
416      public static SASLBindRequest createBindRequest(final String bindDN,
417                                                      final byte[] password,
418                                                      final String mechanism,
419                                                      final String... options)
420             throws LDAPException
421      {
422        return createBindRequest(bindDN, password, mechanism,
423             StaticUtils.toList(options));
424      }
425    
426    
427    
428      /**
429       * Creates a new SASL bind request using the provided information.
430       *
431       * @param  bindDN     The bind DN to use for the SASL bind request.  For most
432       *                    SASL mechanisms, this should be {@code null}, since the
433       *                    identity of the target user should be specified in some
434       *                    other way (e.g., via an "authID" SASL option).
435       * @param  password   The password to use for the SASL bind request.  It may
436       *                    be {@code null} if no password is required for the
437       *                    desired SASL mechanism.
438       * @param  mechanism  The name of the SASL mechanism to use.  It may be
439       *                    {@code null} if the provided set of options contains a
440       *                    "mech" option to specify the desired SASL option.
441       * @param  options    The set of SASL options to use when creating the bind
442       *                    request, in the form "name=value".  It may be
443       *                    {@code null} or empty if no SASL options are needed and
444       *                    a value was provided for the {@code mechanism} argument.
445       *                    If the set of SASL options includes a "mech" option,
446       *                    then the {@code mechanism} argument must be {@code null}
447       *                    or have a value that matches the value of the "mech"
448       *                    SASL option.
449       * @param  controls   The set of controls to include in the request.
450       *
451       * @return  The SASL bind request created using the provided information.
452       *
453       * @throws  LDAPException  If a problem is encountered while trying to create
454       *                         the SASL bind request.
455       */
456      public static SASLBindRequest createBindRequest(final String bindDN,
457                                                      final byte[] password,
458                                                      final String mechanism,
459                                                      final List<String> options,
460                                                      final Control... controls)
461             throws LDAPException
462      {
463        // Parse the provided set of options to ensure that they are properly
464        // formatted in name-value form, and extract the SASL mechanism.
465        final String mech;
466        final Map<String,String> optionsMap = parseOptions(options);
467        final String mechOption =
468             optionsMap.remove(toLowerCase(SASL_OPTION_MECHANISM));
469        if (mechOption != null)
470        {
471          mech = mechOption;
472          if ((mechanism != null) && (! mech.equalsIgnoreCase(mechanism)))
473          {
474            throw new LDAPException(ResultCode.PARAM_ERROR,
475                 ERR_SASL_OPTION_MECH_CONFLICT.get(mechanism, mech));
476          }
477        }
478        else
479        {
480          mech = mechanism;
481        }
482    
483        if (mech == null)
484        {
485          throw new LDAPException(ResultCode.PARAM_ERROR,
486               ERR_SASL_OPTION_NO_MECH.get());
487        }
488    
489        if (mech.equalsIgnoreCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME))
490        {
491          return createANONYMOUSBindRequest(password, optionsMap, controls);
492        }
493        else if (mech.equalsIgnoreCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME))
494        {
495          return createCRAMMD5BindRequest(password, optionsMap, controls);
496        }
497        else if (mech.equalsIgnoreCase(
498                      DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME))
499        {
500          return createDIGESTMD5BindRequest(password, optionsMap, controls);
501        }
502        else if (mech.equalsIgnoreCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME))
503        {
504          return createEXTERNALBindRequest(password, optionsMap, controls);
505        }
506        else if (mech.equalsIgnoreCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME))
507        {
508          return createGSSAPIBindRequest(password, optionsMap, controls);
509        }
510        else if (mech.equalsIgnoreCase(PLAINBindRequest.PLAIN_MECHANISM_NAME))
511        {
512          return createPLAINBindRequest(password, optionsMap, controls);
513        }
514        else
515        {
516          // If Commercial Edition classes are available, then see if the
517          // authentication attempt is for one of the Commercial Edition mechanisms.
518          try
519          {
520            final Class<?> c =
521                 Class.forName("com.unboundid.ldap.sdk.unboundidds.SASLHelper");
522            final Method createBindRequestMethod = c.getMethod("createBindRequest",
523                 String.class, StaticUtils.NO_BYTES.getClass(), String.class,
524                 Map.class, StaticUtils.NO_CONTROLS.getClass());
525            final Object bindRequestObject = createBindRequestMethod.invoke(null,
526                 bindDN, password, mech, optionsMap, controls);
527            if (bindRequestObject != null)
528            {
529              return (SASLBindRequest) bindRequestObject;
530            }
531          }
532          catch (final Exception e)
533          {
534            Debug.debugException(e);
535    
536            // This may mean that there was a problem with the provided arguments.
537            // If it's an InvocationTargetException that wraps an LDAPException,
538            // then throw that LDAPException.
539            if (e instanceof InvocationTargetException)
540            {
541              final InvocationTargetException ite = (InvocationTargetException) e;
542              final Throwable t = ite.getTargetException();
543              if (t instanceof LDAPException)
544              {
545                throw (LDAPException) t;
546              }
547            }
548          }
549    
550          throw new LDAPException(ResultCode.PARAM_ERROR,
551               ERR_SASL_OPTION_UNSUPPORTED_MECH.get(mech));
552        }
553      }
554    
555    
556    
557      /**
558       * Creates a SASL ANONYMOUS bind request using the provided set of options.
559       *
560       * @param  password  The password to use for the bind request.
561       * @param  options   The set of SASL options for the bind request.
562       * @param  controls  The set of controls to include in the request.
563       *
564       * @return  The SASL ANONYMOUS bind request that was created.
565       *
566       * @throws  LDAPException  If a problem is encountered while trying to create
567       *                         the SASL bind request.
568       */
569      private static ANONYMOUSBindRequest createANONYMOUSBindRequest(
570                                               final byte[] password,
571                                               final Map<String,String> options,
572                                               final Control[] controls)
573              throws LDAPException
574      {
575        if (password != null)
576        {
577          throw new LDAPException(ResultCode.PARAM_ERROR,
578               ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
579                    ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME));
580        }
581    
582    
583        // The trace option is optional.
584        final String trace = options.remove(toLowerCase(SASL_OPTION_TRACE));
585    
586        // Ensure no unsupported options were provided.
587        ensureNoUnsupportedOptions(options,
588             ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME);
589    
590        return new ANONYMOUSBindRequest(trace, controls);
591      }
592    
593    
594    
595      /**
596       * Creates a SASL CRAM-MD5 bind request using the provided password and set of
597       * options.
598       *
599       * @param  password  The password to use for the bind request.
600       * @param  options   The set of SASL options for the bind request.
601       * @param  controls  The set of controls to include in the request.
602       *
603       * @return  The SASL CRAM-MD5 bind request that was created.
604       *
605       * @throws  LDAPException  If a problem is encountered while trying to create
606       *                         the SASL bind request.
607       */
608      private static CRAMMD5BindRequest createCRAMMD5BindRequest(
609                                             final byte[] password,
610                                             final Map<String,String> options,
611                                             final Control[] controls)
612              throws LDAPException
613      {
614        if (password == null)
615        {
616          throw new LDAPException(ResultCode.PARAM_ERROR,
617               ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
618                    CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
619        }
620    
621    
622        // The authID option is required.
623        final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
624        if (authID == null)
625        {
626          throw new LDAPException(ResultCode.PARAM_ERROR,
627               ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
628                    CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
629        }
630    
631    
632        // Ensure no unsupported options were provided.
633        ensureNoUnsupportedOptions(options,
634             CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME);
635    
636        return new CRAMMD5BindRequest(authID, password, controls);
637      }
638    
639    
640    
641      /**
642       * Creates a SASL DIGEST-MD5 bind request using the provided password and set
643       * of options.
644       *
645       * @param  password  The password to use for the bind request.
646       * @param  options   The set of SASL options for the bind request.
647       * @param  controls  The set of controls to include in the request.
648       *
649       * @return  The SASL DIGEST-MD5 bind request that was created.
650       *
651       * @throws  LDAPException  If a problem is encountered while trying to create
652       *                         the SASL bind request.
653       */
654      private static DIGESTMD5BindRequest createDIGESTMD5BindRequest(
655                                               final byte[] password,
656                                               final Map<String,String> options,
657                                               final Control[] controls)
658              throws LDAPException
659      {
660        if (password == null)
661        {
662          throw new LDAPException(ResultCode.PARAM_ERROR,
663               ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
664                    DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME));
665        }
666    
667        // The authID option is required.
668        final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
669        if (authID == null)
670        {
671          throw new LDAPException(ResultCode.PARAM_ERROR,
672               ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
673                    CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
674        }
675    
676        // The authzID option is optional.
677        final String authzID = options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID));
678    
679        // The realm option is optional.
680        final String realm = options.remove(toLowerCase(SASL_OPTION_REALM));
681    
682        // Ensure no unsupported options were provided.
683        ensureNoUnsupportedOptions(options,
684             DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME);
685    
686        return new DIGESTMD5BindRequest(authID, authzID, password, realm, controls);
687      }
688    
689    
690    
691      /**
692       * Creates a SASL EXTERNAL bind request using the provided set of options.
693       *
694       * @param  password  The password to use for the bind request.
695       * @param  options   The set of SASL options for the bind request.
696       * @param  controls  The set of controls to include in the request.
697       *
698       * @return  The SASL EXTERNAL bind request that was created.
699       *
700       * @throws  LDAPException  If a problem is encountered while trying to create
701       *                         the SASL bind request.
702       */
703      private static EXTERNALBindRequest createEXTERNALBindRequest(
704                                              final byte[] password,
705                                              final Map<String,String> options,
706                                              final Control[] controls)
707              throws LDAPException
708      {
709        if (password != null)
710        {
711          throw new LDAPException(ResultCode.PARAM_ERROR,
712               ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
713                    EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME));
714        }
715    
716        // Ensure no unsupported options were provided.
717        ensureNoUnsupportedOptions(options,
718             EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME);
719    
720        return new EXTERNALBindRequest(controls);
721      }
722    
723    
724    
725      /**
726       * Creates a SASL GSSAPI bind request using the provided password and set of
727       * options.
728       *
729       * @param  password  The password to use for the bind request.
730       * @param  options   The set of SASL options for the bind request.
731       * @param  controls  The set of controls to include in the request.
732       *
733       * @return  The SASL GSSAPI bind request that was created.
734       *
735       * @throws  LDAPException  If a problem is encountered while trying to create
736       *                         the SASL bind request.
737       */
738      private static GSSAPIBindRequest createGSSAPIBindRequest(
739                                            final byte[] password,
740                                            final Map<String,String> options,
741                                            final Control[] controls)
742              throws LDAPException
743      {
744        // The authID option is required.
745        final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
746        if (authID == null)
747        {
748          throw new LDAPException(ResultCode.PARAM_ERROR,
749               ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
750                    GSSAPIBindRequest.GSSAPI_MECHANISM_NAME));
751        }
752        final GSSAPIBindRequestProperties gssapiProperties =
753             new GSSAPIBindRequestProperties(authID, password);
754    
755        // The authzID option is optional.
756        gssapiProperties.setAuthorizationID(
757             options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID)));
758    
759        // The configFile option is optional.
760        gssapiProperties.setConfigFilePath(options.remove(toLowerCase(
761             SASL_OPTION_CONFIG_FILE)));
762    
763        // The debug option is optional.
764        gssapiProperties.setEnableGSSAPIDebugging(getBooleanValue(options,
765             SASL_OPTION_DEBUG, false));
766    
767        // The kdcAddress option is optional.
768        gssapiProperties.setKDCAddress(options.remove(
769             toLowerCase(SASL_OPTION_KDC_ADDRESS)));
770    
771        // The protocol option is optional.
772        final String protocol = options.remove(toLowerCase(SASL_OPTION_PROTOCOL));
773        if (protocol != null)
774        {
775          gssapiProperties.setServicePrincipalProtocol(protocol);
776        }
777    
778        // The realm option is optional.
779        gssapiProperties.setRealm(options.remove(toLowerCase(SASL_OPTION_REALM)));
780    
781        // The renewTGT option is optional.
782        gssapiProperties.setRenewTGT(getBooleanValue(options, SASL_OPTION_RENEW_TGT,
783             false));
784    
785        // The requireCache option is optional.
786        gssapiProperties.setRequireCachedCredentials(getBooleanValue(options,
787             SASL_OPTION_REQUIRE_CACHE, false));
788    
789        // The ticketCache option is optional.
790        gssapiProperties.setTicketCachePath(options.remove(toLowerCase(
791             SASL_OPTION_TICKET_CACHE_PATH)));
792    
793        // The useTicketCache option is optional.
794        gssapiProperties.setUseTicketCache(getBooleanValue(options,
795             SASL_OPTION_USE_TICKET_CACHE, true));
796    
797        // Ensure no unsupported options were provided.
798        ensureNoUnsupportedOptions(options,
799             GSSAPIBindRequest.GSSAPI_MECHANISM_NAME);
800    
801        // A password must have been provided unless useTicketCache=true and
802        // requireTicketCache=true.
803        if (password == null)
804        {
805          if (! (gssapiProperties.useTicketCache() &&
806               gssapiProperties.requireCachedCredentials()))
807          {
808            throw new LDAPException(ResultCode.PARAM_ERROR,
809                 ERR_SASL_OPTION_GSSAPI_PASSWORD_REQUIRED.get());
810          }
811        }
812    
813        return new GSSAPIBindRequest(gssapiProperties, controls);
814      }
815    
816    
817    
818      /**
819       * Creates a SASL PLAIN bind request using the provided password and set of
820       * options.
821       *
822       * @param  password  The password to use for the bind request.
823       * @param  options   The set of SASL options for the bind request.
824       * @param  controls  The set of controls to include in the request.
825       *
826       * @return  The SASL PLAIN bind request that was created.
827       *
828       * @throws  LDAPException  If a problem is encountered while trying to create
829       *                         the SASL bind request.
830       */
831      private static PLAINBindRequest createPLAINBindRequest(
832                                            final byte[] password,
833                                            final Map<String,String> options,
834                                            final Control[] controls)
835              throws LDAPException
836      {
837        if (password == null)
838        {
839          throw new LDAPException(ResultCode.PARAM_ERROR,
840               ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
841                    PLAINBindRequest.PLAIN_MECHANISM_NAME));
842        }
843    
844        // The authID option is required.
845        final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
846        if (authID == null)
847        {
848          throw new LDAPException(ResultCode.PARAM_ERROR,
849               ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
850                    PLAINBindRequest.PLAIN_MECHANISM_NAME));
851        }
852    
853        // The authzID option is optional.
854        final String authzID = options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID));
855    
856        // Ensure no unsupported options were provided.
857        ensureNoUnsupportedOptions(options,
858             PLAINBindRequest.PLAIN_MECHANISM_NAME);
859    
860        return new PLAINBindRequest(authID, authzID, password, controls);
861      }
862    
863    
864    
865      /**
866       * Parses the provided list of SASL options.
867       *
868       * @param  options  The list of options to be parsed.
869       *
870       * @return  A map with the parsed set of options.
871       *
872       * @throws  LDAPException  If a problem is encountered while parsing options.
873       */
874      private static Map<String,String>
875                          parseOptions(final List<String> options)
876              throws LDAPException
877      {
878        if (options == null)
879        {
880          return new HashMap<String,String>(0);
881        }
882    
883        final HashMap<String,String> m = new HashMap<String,String>(options.size());
884        for (final String s : options)
885        {
886          final int equalPos = s.indexOf('=');
887          if (equalPos < 0)
888          {
889            throw new LDAPException(ResultCode.PARAM_ERROR,
890                 ERR_SASL_OPTION_MISSING_EQUAL.get(s));
891          }
892          else if (equalPos == 0)
893          {
894            throw new LDAPException(ResultCode.PARAM_ERROR,
895                 ERR_SASL_OPTION_STARTS_WITH_EQUAL.get(s));
896          }
897    
898          final String name = s.substring(0, equalPos);
899          final String value = s.substring(equalPos + 1);
900          if (m.put(toLowerCase(name), value) != null)
901          {
902            throw new LDAPException(ResultCode.PARAM_ERROR,
903                 ERR_SASL_OPTION_NOT_MULTI_VALUED.get(name));
904          }
905        }
906    
907        return m;
908      }
909    
910    
911    
912      /**
913       * Ensures that the provided map is empty, and will throw an exception if it
914       * isn't.  This method is intended for internal use only.
915       *
916       * @param  options    The map of options to ensure is empty.
917       * @param  mechanism  The associated SASL mechanism.
918       *
919       * @throws  LDAPException  If the map of SASL options is not empty.
920       */
921      @InternalUseOnly()
922      public static void ensureNoUnsupportedOptions(
923                              final Map<String,String> options,
924                              final String mechanism)
925              throws LDAPException
926      {
927        if (! options.isEmpty())
928        {
929          for (final String s : options.keySet())
930          {
931            throw new LDAPException(ResultCode.PARAM_ERROR,
932                 ERR_SASL_OPTION_UNSUPPORTED_FOR_MECH.get(s,mechanism));
933          }
934        }
935      }
936    
937    
938    
939      /**
940       * Retrieves the value of the specified option and parses it as a boolean.
941       * Values of "true", "t", "yes", "y", "on", and "1" will be treated as
942       * {@code true}.  Values of "false", "f", "no", "n", "off", and "0" will be
943       * treated as {@code false}.
944       *
945       * @param  m  The map from which to retrieve the option.  It must not be
946       *            {@code null}.
947       * @param  o  The name of the option to examine.
948       * @param  d  The default value to use if the given option was not provided.
949       *
950       * @return  The parsed boolean value.
951       *
952       * @throws  LDAPException  If the option value cannot be parsed as a boolean.
953       */
954      static boolean getBooleanValue(final Map<String,String> m, final String o,
955                                     final boolean d)
956             throws LDAPException
957      {
958        final String s = toLowerCase(m.remove(toLowerCase(o)));
959        if (s == null)
960        {
961          return d;
962        }
963        else if (s.equals("true") ||
964                 s.equals("t") ||
965                 s.equals("yes") ||
966                 s.equals("y") ||
967                 s.equals("on") ||
968                 s.equals("1"))
969        {
970          return true;
971        }
972        else if (s.equals("false") ||
973                 s.equals("f") ||
974                 s.equals("no") ||
975                 s.equals("n") ||
976                 s.equals("off") ||
977                 s.equals("0"))
978        {
979          return false;
980        }
981        else
982        {
983          throw new LDAPException(ResultCode.PARAM_ERROR,
984               ERR_SASL_OPTION_MALFORMED_BOOLEAN_VALUE.get(o));
985        }
986      }
987    }