001    /*
002     * Copyright 2011-2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2011-2015 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        // Parse the provided set of options to ensure that they are properly
479        // formatted in name-value form, and extract the SASL mechanism.
480        final String mech;
481        final Map<String,String> optionsMap = parseOptions(options);
482        final String mechOption =
483             optionsMap.remove(toLowerCase(SASL_OPTION_MECHANISM));
484        if (mechOption != null)
485        {
486          mech = mechOption;
487          if ((mechanism != null) && (! mech.equalsIgnoreCase(mechanism)))
488          {
489            throw new LDAPException(ResultCode.PARAM_ERROR,
490                 ERR_SASL_OPTION_MECH_CONFLICT.get(mechanism, mech));
491          }
492        }
493        else
494        {
495          mech = mechanism;
496        }
497    
498        if (mech == null)
499        {
500          throw new LDAPException(ResultCode.PARAM_ERROR,
501               ERR_SASL_OPTION_NO_MECH.get());
502        }
503    
504        if (mech.equalsIgnoreCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME))
505        {
506          return createANONYMOUSBindRequest(password, optionsMap, controls);
507        }
508        else if (mech.equalsIgnoreCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME))
509        {
510          return createCRAMMD5BindRequest(password, optionsMap, controls);
511        }
512        else if (mech.equalsIgnoreCase(
513                      DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME))
514        {
515          return createDIGESTMD5BindRequest(password, optionsMap, controls);
516        }
517        else if (mech.equalsIgnoreCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME))
518        {
519          return createEXTERNALBindRequest(password, optionsMap, controls);
520        }
521        else if (mech.equalsIgnoreCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME))
522        {
523          return createGSSAPIBindRequest(password, optionsMap, controls);
524        }
525        else if (mech.equalsIgnoreCase(PLAINBindRequest.PLAIN_MECHANISM_NAME))
526        {
527          return createPLAINBindRequest(password, optionsMap, controls);
528        }
529        else
530        {
531          // If Commercial Edition classes are available, then see if the
532          // authentication attempt is for one of the Commercial Edition mechanisms.
533          try
534          {
535            final Class<?> c =
536                 Class.forName("com.unboundid.ldap.sdk.unboundidds.SASLHelper");
537            final Method createBindRequestMethod = c.getMethod("createBindRequest",
538                 String.class, StaticUtils.NO_BYTES.getClass(), String.class,
539                 Map.class, StaticUtils.NO_CONTROLS.getClass());
540            final Object bindRequestObject = createBindRequestMethod.invoke(null,
541                 bindDN, password, mech, optionsMap, controls);
542            if (bindRequestObject != null)
543            {
544              return (SASLBindRequest) bindRequestObject;
545            }
546          }
547          catch (final Exception e)
548          {
549            Debug.debugException(e);
550    
551            // This may mean that there was a problem with the provided arguments.
552            // If it's an InvocationTargetException that wraps an LDAPException,
553            // then throw that LDAPException.
554            if (e instanceof InvocationTargetException)
555            {
556              final InvocationTargetException ite = (InvocationTargetException) e;
557              final Throwable t = ite.getTargetException();
558              if (t instanceof LDAPException)
559              {
560                throw (LDAPException) t;
561              }
562            }
563          }
564    
565          throw new LDAPException(ResultCode.PARAM_ERROR,
566               ERR_SASL_OPTION_UNSUPPORTED_MECH.get(mech));
567        }
568      }
569    
570    
571    
572      /**
573       * Creates a SASL ANONYMOUS bind request using the provided set of options.
574       *
575       * @param  password  The password to use for the bind request.
576       * @param  options   The set of SASL options for the bind request.
577       * @param  controls  The set of controls to include in the request.
578       *
579       * @return  The SASL ANONYMOUS bind request that was created.
580       *
581       * @throws  LDAPException  If a problem is encountered while trying to create
582       *                         the SASL bind request.
583       */
584      private static ANONYMOUSBindRequest createANONYMOUSBindRequest(
585                                               final byte[] password,
586                                               final Map<String,String> options,
587                                               final Control[] controls)
588              throws LDAPException
589      {
590        if (password != null)
591        {
592          throw new LDAPException(ResultCode.PARAM_ERROR,
593               ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
594                    ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME));
595        }
596    
597    
598        // The trace option is optional.
599        final String trace = options.remove(toLowerCase(SASL_OPTION_TRACE));
600    
601        // Ensure no unsupported options were provided.
602        ensureNoUnsupportedOptions(options,
603             ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME);
604    
605        return new ANONYMOUSBindRequest(trace, controls);
606      }
607    
608    
609    
610      /**
611       * Creates a SASL CRAM-MD5 bind request using the provided password and set of
612       * options.
613       *
614       * @param  password  The password to use for the bind request.
615       * @param  options   The set of SASL options for the bind request.
616       * @param  controls  The set of controls to include in the request.
617       *
618       * @return  The SASL CRAM-MD5 bind request that was created.
619       *
620       * @throws  LDAPException  If a problem is encountered while trying to create
621       *                         the SASL bind request.
622       */
623      private static CRAMMD5BindRequest createCRAMMD5BindRequest(
624                                             final byte[] password,
625                                             final Map<String,String> options,
626                                             final Control[] controls)
627              throws LDAPException
628      {
629        if (password == null)
630        {
631          throw new LDAPException(ResultCode.PARAM_ERROR,
632               ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
633                    CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
634        }
635    
636    
637        // The authID option is required.
638        final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
639        if (authID == null)
640        {
641          throw new LDAPException(ResultCode.PARAM_ERROR,
642               ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
643                    CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
644        }
645    
646    
647        // Ensure no unsupported options were provided.
648        ensureNoUnsupportedOptions(options,
649             CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME);
650    
651        return new CRAMMD5BindRequest(authID, password, controls);
652      }
653    
654    
655    
656      /**
657       * Creates a SASL DIGEST-MD5 bind request using the provided password and set
658       * of options.
659       *
660       * @param  password  The password to use for the bind request.
661       * @param  options   The set of SASL options for the bind request.
662       * @param  controls  The set of controls to include in the request.
663       *
664       * @return  The SASL DIGEST-MD5 bind request that was created.
665       *
666       * @throws  LDAPException  If a problem is encountered while trying to create
667       *                         the SASL bind request.
668       */
669      private static DIGESTMD5BindRequest createDIGESTMD5BindRequest(
670                                               final byte[] password,
671                                               final Map<String,String> options,
672                                               final Control[] controls)
673              throws LDAPException
674      {
675        if (password == null)
676        {
677          throw new LDAPException(ResultCode.PARAM_ERROR,
678               ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
679                    DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME));
680        }
681    
682        // The authID option is required.
683        final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
684        if (authID == null)
685        {
686          throw new LDAPException(ResultCode.PARAM_ERROR,
687               ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
688                    CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
689        }
690    
691        final DIGESTMD5BindRequestProperties properties =
692             new DIGESTMD5BindRequestProperties(authID, password);
693    
694        // The authzID option is optional.
695        properties.setAuthorizationID(
696             options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID)));
697    
698        // The realm option is optional.
699        properties.setRealm(options.remove(toLowerCase(SASL_OPTION_REALM)));
700    
701        // The QoP option is optional, and may contain multiple values that need to
702        // be parsed.
703        final String qopString = options.remove(toLowerCase(SASL_OPTION_QOP));
704        if (qopString != null)
705        {
706          properties.setAllowedQoP(
707               SASLQualityOfProtection.decodeQoPList(qopString));
708        }
709    
710        // Ensure no unsupported options were provided.
711        ensureNoUnsupportedOptions(options,
712             DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME);
713    
714        return new DIGESTMD5BindRequest(properties, controls);
715      }
716    
717    
718    
719      /**
720       * Creates a SASL EXTERNAL bind request using the provided set of options.
721       *
722       * @param  password  The password to use for the bind request.
723       * @param  options   The set of SASL options for the bind request.
724       * @param  controls  The set of controls to include in the request.
725       *
726       * @return  The SASL EXTERNAL bind request that was created.
727       *
728       * @throws  LDAPException  If a problem is encountered while trying to create
729       *                         the SASL bind request.
730       */
731      private static EXTERNALBindRequest createEXTERNALBindRequest(
732                                              final byte[] password,
733                                              final Map<String,String> options,
734                                              final Control[] controls)
735              throws LDAPException
736      {
737        if (password != null)
738        {
739          throw new LDAPException(ResultCode.PARAM_ERROR,
740               ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
741                    EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME));
742        }
743    
744        // Ensure no unsupported options were provided.
745        ensureNoUnsupportedOptions(options,
746             EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME);
747    
748        return new EXTERNALBindRequest(controls);
749      }
750    
751    
752    
753      /**
754       * Creates a SASL GSSAPI bind request using the provided password and set of
755       * options.
756       *
757       * @param  password  The password to use for the bind request.
758       * @param  options   The set of SASL options for the bind request.
759       * @param  controls  The set of controls to include in the request.
760       *
761       * @return  The SASL GSSAPI bind request that was created.
762       *
763       * @throws  LDAPException  If a problem is encountered while trying to create
764       *                         the SASL bind request.
765       */
766      private static GSSAPIBindRequest createGSSAPIBindRequest(
767                                            final byte[] password,
768                                            final Map<String,String> options,
769                                            final Control[] controls)
770              throws LDAPException
771      {
772        // The authID option is required.
773        final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
774        if (authID == null)
775        {
776          throw new LDAPException(ResultCode.PARAM_ERROR,
777               ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
778                    GSSAPIBindRequest.GSSAPI_MECHANISM_NAME));
779        }
780        final GSSAPIBindRequestProperties gssapiProperties =
781             new GSSAPIBindRequestProperties(authID, password);
782    
783        // The authzID option is optional.
784        gssapiProperties.setAuthorizationID(
785             options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID)));
786    
787        // The configFile option is optional.
788        gssapiProperties.setConfigFilePath(options.remove(toLowerCase(
789             SASL_OPTION_CONFIG_FILE)));
790    
791        // The debug option is optional.
792        gssapiProperties.setEnableGSSAPIDebugging(getBooleanValue(options,
793             SASL_OPTION_DEBUG, false));
794    
795        // The kdcAddress option is optional.
796        gssapiProperties.setKDCAddress(options.remove(
797             toLowerCase(SASL_OPTION_KDC_ADDRESS)));
798    
799        // The protocol option is optional.
800        final String protocol = options.remove(toLowerCase(SASL_OPTION_PROTOCOL));
801        if (protocol != null)
802        {
803          gssapiProperties.setServicePrincipalProtocol(protocol);
804        }
805    
806        // The realm option is optional.
807        gssapiProperties.setRealm(options.remove(toLowerCase(SASL_OPTION_REALM)));
808    
809        // The QoP option is optional, and may contain multiple values that need to
810        // be parsed.
811        final String qopString = options.remove(toLowerCase(SASL_OPTION_QOP));
812        if (qopString != null)
813        {
814          gssapiProperties.setAllowedQoP(
815               SASLQualityOfProtection.decodeQoPList(qopString));
816        }
817    
818        // The renewTGT option is optional.
819        gssapiProperties.setRenewTGT(getBooleanValue(options, SASL_OPTION_RENEW_TGT,
820             false));
821    
822        // The requireCache option is optional.
823        gssapiProperties.setRequireCachedCredentials(getBooleanValue(options,
824             SASL_OPTION_REQUIRE_CACHE, false));
825    
826        // The ticketCache option is optional.
827        gssapiProperties.setTicketCachePath(options.remove(toLowerCase(
828             SASL_OPTION_TICKET_CACHE_PATH)));
829    
830        // The useTicketCache option is optional.
831        gssapiProperties.setUseTicketCache(getBooleanValue(options,
832             SASL_OPTION_USE_TICKET_CACHE, true));
833    
834        // Ensure no unsupported options were provided.
835        ensureNoUnsupportedOptions(options,
836             GSSAPIBindRequest.GSSAPI_MECHANISM_NAME);
837    
838        // A password must have been provided unless useTicketCache=true and
839        // requireTicketCache=true.
840        if (password == null)
841        {
842          if (! (gssapiProperties.useTicketCache() &&
843               gssapiProperties.requireCachedCredentials()))
844          {
845            throw new LDAPException(ResultCode.PARAM_ERROR,
846                 ERR_SASL_OPTION_GSSAPI_PASSWORD_REQUIRED.get());
847          }
848        }
849    
850        return new GSSAPIBindRequest(gssapiProperties, controls);
851      }
852    
853    
854    
855      /**
856       * Creates a SASL PLAIN bind request using the provided password and set of
857       * options.
858       *
859       * @param  password  The password to use for the bind request.
860       * @param  options   The set of SASL options for the bind request.
861       * @param  controls  The set of controls to include in the request.
862       *
863       * @return  The SASL PLAIN bind request that was created.
864       *
865       * @throws  LDAPException  If a problem is encountered while trying to create
866       *                         the SASL bind request.
867       */
868      private static PLAINBindRequest createPLAINBindRequest(
869                                            final byte[] password,
870                                            final Map<String,String> options,
871                                            final Control[] controls)
872              throws LDAPException
873      {
874        if (password == null)
875        {
876          throw new LDAPException(ResultCode.PARAM_ERROR,
877               ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
878                    PLAINBindRequest.PLAIN_MECHANISM_NAME));
879        }
880    
881        // The authID option is required.
882        final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
883        if (authID == null)
884        {
885          throw new LDAPException(ResultCode.PARAM_ERROR,
886               ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
887                    PLAINBindRequest.PLAIN_MECHANISM_NAME));
888        }
889    
890        // The authzID option is optional.
891        final String authzID = options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID));
892    
893        // Ensure no unsupported options were provided.
894        ensureNoUnsupportedOptions(options,
895             PLAINBindRequest.PLAIN_MECHANISM_NAME);
896    
897        return new PLAINBindRequest(authID, authzID, password, controls);
898      }
899    
900    
901    
902      /**
903       * Parses the provided list of SASL options.
904       *
905       * @param  options  The list of options to be parsed.
906       *
907       * @return  A map with the parsed set of options.
908       *
909       * @throws  LDAPException  If a problem is encountered while parsing options.
910       */
911      private static Map<String,String>
912                          parseOptions(final List<String> options)
913              throws LDAPException
914      {
915        if (options == null)
916        {
917          return new HashMap<String,String>(0);
918        }
919    
920        final HashMap<String,String> m = new HashMap<String,String>(options.size());
921        for (final String s : options)
922        {
923          final int equalPos = s.indexOf('=');
924          if (equalPos < 0)
925          {
926            throw new LDAPException(ResultCode.PARAM_ERROR,
927                 ERR_SASL_OPTION_MISSING_EQUAL.get(s));
928          }
929          else if (equalPos == 0)
930          {
931            throw new LDAPException(ResultCode.PARAM_ERROR,
932                 ERR_SASL_OPTION_STARTS_WITH_EQUAL.get(s));
933          }
934    
935          final String name = s.substring(0, equalPos);
936          final String value = s.substring(equalPos + 1);
937          if (m.put(toLowerCase(name), value) != null)
938          {
939            throw new LDAPException(ResultCode.PARAM_ERROR,
940                 ERR_SASL_OPTION_NOT_MULTI_VALUED.get(name));
941          }
942        }
943    
944        return m;
945      }
946    
947    
948    
949      /**
950       * Ensures that the provided map is empty, and will throw an exception if it
951       * isn't.  This method is intended for internal use only.
952       *
953       * @param  options    The map of options to ensure is empty.
954       * @param  mechanism  The associated SASL mechanism.
955       *
956       * @throws  LDAPException  If the map of SASL options is not empty.
957       */
958      @InternalUseOnly()
959      public static void ensureNoUnsupportedOptions(
960                              final Map<String,String> options,
961                              final String mechanism)
962              throws LDAPException
963      {
964        if (! options.isEmpty())
965        {
966          for (final String s : options.keySet())
967          {
968            throw new LDAPException(ResultCode.PARAM_ERROR,
969                 ERR_SASL_OPTION_UNSUPPORTED_FOR_MECH.get(s,mechanism));
970          }
971        }
972      }
973    
974    
975    
976      /**
977       * Retrieves the value of the specified option and parses it as a boolean.
978       * Values of "true", "t", "yes", "y", "on", and "1" will be treated as
979       * {@code true}.  Values of "false", "f", "no", "n", "off", and "0" will be
980       * treated as {@code false}.
981       *
982       * @param  m  The map from which to retrieve the option.  It must not be
983       *            {@code null}.
984       * @param  o  The name of the option to examine.
985       * @param  d  The default value to use if the given option was not provided.
986       *
987       * @return  The parsed boolean value.
988       *
989       * @throws  LDAPException  If the option value cannot be parsed as a boolean.
990       */
991      static boolean getBooleanValue(final Map<String,String> m, final String o,
992                                     final boolean d)
993             throws LDAPException
994      {
995        final String s = toLowerCase(m.remove(toLowerCase(o)));
996        if (s == null)
997        {
998          return d;
999        }
1000        else if (s.equals("true") ||
1001                 s.equals("t") ||
1002                 s.equals("yes") ||
1003                 s.equals("y") ||
1004                 s.equals("on") ||
1005                 s.equals("1"))
1006        {
1007          return true;
1008        }
1009        else if (s.equals("false") ||
1010                 s.equals("f") ||
1011                 s.equals("no") ||
1012                 s.equals("n") ||
1013                 s.equals("off") ||
1014                 s.equals("0"))
1015        {
1016          return false;
1017        }
1018        else
1019        {
1020          throw new LDAPException(ResultCode.PARAM_ERROR,
1021               ERR_SASL_OPTION_MALFORMED_BOOLEAN_VALUE.get(o));
1022        }
1023      }
1024    }