001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2007-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk;
037
038
039
040import java.nio.charset.StandardCharsets;
041import java.util.ArrayList;
042import java.util.List;
043import java.util.concurrent.LinkedBlockingQueue;
044import java.util.concurrent.TimeUnit;
045import java.util.logging.Level;
046
047import com.unboundid.asn1.ASN1OctetString;
048import com.unboundid.ldap.protocol.BindRequestProtocolOp;
049import com.unboundid.ldap.protocol.LDAPMessage;
050import com.unboundid.ldap.protocol.LDAPResponse;
051import com.unboundid.util.Debug;
052import com.unboundid.util.Extensible;
053import com.unboundid.util.InternalUseOnly;
054import com.unboundid.util.NotNull;
055import com.unboundid.util.Nullable;
056import com.unboundid.util.StaticUtils;
057import com.unboundid.util.ThreadSafety;
058import com.unboundid.util.ThreadSafetyLevel;
059
060import static com.unboundid.ldap.sdk.LDAPMessages.*;
061
062
063
064/**
065 * This class provides an API that should be used to represent an LDAPv3 SASL
066 * bind request.  A SASL bind includes a SASL mechanism name and an optional set
067 * of credentials.
068 * <BR><BR>
069 * See <A HREF="http://www.ietf.org/rfc/rfc4422.txt">RFC 4422</A> for more
070 * information about the Simple Authentication and Security Layer.
071 */
072@Extensible()
073@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE)
074public abstract class SASLBindRequest
075       extends BindRequest
076       implements ResponseAcceptor
077{
078  /**
079   * The BER type to use for the credentials element in a simple bind request
080   * protocol op.
081   */
082  protected static final byte CRED_TYPE_SASL = (byte) 0xA3;
083
084
085
086  /**
087   * The serial version UID for this serializable class.
088   */
089  private static final long serialVersionUID = -5842126553864908312L;
090
091
092
093  // The message ID to use for LDAP messages used in bind processing.
094  private int messageID;
095
096  // The queue used to receive responses from the server.
097  @NotNull private final LinkedBlockingQueue<LDAPResponse> responseQueue;
098
099
100
101  /**
102   * Creates a new SASL bind request with the provided controls.
103   *
104   * @param  controls  The set of controls to include in this SASL bind request.
105   */
106  protected SASLBindRequest(@Nullable final Control[] controls)
107  {
108    super(controls);
109
110    messageID     = -1;
111    responseQueue = new LinkedBlockingQueue<>();
112  }
113
114
115
116  /**
117   * {@inheritDoc}
118   */
119  @Override()
120  @NotNull()
121  public String getBindType()
122  {
123    return getSASLMechanismName();
124  }
125
126
127
128  /**
129   * Retrieves the name of the SASL mechanism used in this SASL bind request.
130   *
131   * @return  The name of the SASL mechanism used in this SASL bind request.
132   */
133  @NotNull()
134  public abstract String getSASLMechanismName();
135
136
137
138  /**
139   * {@inheritDoc}
140   */
141  @Override()
142  public int getLastMessageID()
143  {
144    return messageID;
145  }
146
147
148
149  /**
150   * Sends an LDAP message to the directory server and waits for the response.
151   *
152   * @param  connection       The connection to the directory server.
153   * @param  bindDN           The bind DN to use for the request.  It should be
154   *                          {@code null} for most types of SASL bind requests.
155   * @param  saslCredentials  The SASL credentials to use for the bind request.
156   *                          It may be {@code null} if no credentials are
157   *                          required.
158   * @param  controls         The set of controls to include in the request.  It
159   *                          may be {@code null} if no controls are required.
160   * @param  timeoutMillis    The maximum length of time in milliseconds to wait
161   *                          for a response, or zero if it should wait forever.
162   *
163   * @return  The bind response message returned by the directory server.
164   *
165   * @throws  LDAPException  If a problem occurs while sending the request or
166   *                         reading the response, or if a timeout occurred
167   *                         while waiting for the response.
168   */
169  @NotNull()
170  protected final BindResult sendBindRequest(
171                       @NotNull final LDAPConnection connection,
172                       @Nullable final String bindDN,
173                       @Nullable final ASN1OctetString saslCredentials,
174                       @Nullable final Control[] controls,
175                       final long timeoutMillis)
176            throws LDAPException
177  {
178    messageID = connection.nextMessageID();
179
180    final BindRequestProtocolOp protocolOp =
181         new BindRequestProtocolOp(bindDN, getSASLMechanismName(),
182                                   saslCredentials);
183
184    final LDAPMessage requestMessage =
185         new LDAPMessage(messageID, protocolOp, controls);
186    return sendMessage(connection, requestMessage, timeoutMillis);
187  }
188
189
190
191  /**
192   * Sends an LDAP message to the directory server and waits for the response.
193   *
194   * @param  connection      The connection to the directory server.
195   * @param  requestMessage  The LDAP message to send to the directory server.
196   * @param  timeoutMillis   The maximum length of time in milliseconds to wait
197   *                         for a response, or zero if it should wait forever.
198   *
199   * @return  The response message received from the server.
200   *
201   * @throws  LDAPException  If a problem occurs while sending the request or
202   *                         reading the response, or if a timeout occurred
203   *                         while waiting for the response.
204   */
205  @NotNull()
206  protected final BindResult sendMessage(
207                                  @NotNull final LDAPConnection connection,
208                                  @NotNull final LDAPMessage requestMessage,
209                                  final long timeoutMillis)
210            throws LDAPException
211  {
212    if (connection.synchronousMode())
213    {
214      return sendMessageSync(connection, requestMessage, timeoutMillis);
215    }
216
217    final int msgID = requestMessage.getMessageID();
218    connection.registerResponseAcceptor(msgID, this);
219    try
220    {
221      Debug.debugLDAPRequest(Level.INFO, this, msgID, connection);
222
223      final LDAPConnectionLogger logger =
224           connection.getConnectionOptions().getConnectionLogger();
225      if (logger != null)
226      {
227        logger.logBindRequest(connection, messageID, this);
228      }
229
230      final long requestTime = System.nanoTime();
231      connection.getConnectionStatistics().incrementNumBindRequests();
232      connection.sendMessage(requestMessage, timeoutMillis);
233
234      // Wait for and process the response.
235      final LDAPResponse response;
236      try
237      {
238        if (timeoutMillis > 0)
239        {
240          response = responseQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS);
241        }
242        else
243        {
244          response = responseQueue.take();
245        }
246      }
247      catch (final InterruptedException ie)
248      {
249        Debug.debugException(ie);
250        Thread.currentThread().interrupt();
251        throw new LDAPException(ResultCode.LOCAL_ERROR,
252             ERR_BIND_INTERRUPTED.get(connection.getHostPort()), ie);
253      }
254
255      return handleResponse(connection, response, requestTime);
256    }
257    finally
258    {
259      connection.deregisterResponseAcceptor(msgID);
260    }
261  }
262
263
264
265  /**
266   * Sends an LDAP message to the directory server and waits for the response.
267   * This should only be used when the connection is operating in synchronous
268   * mode.
269   *
270   * @param  connection      The connection to the directory server.
271   * @param  requestMessage  The LDAP message to send to the directory server.
272   * @param  timeoutMillis   The maximum length of time in milliseconds to wait
273   *                         for a response, or zero if it should wait forever.
274   *
275   * @return  The response message received from the server.
276   *
277   * @throws  LDAPException  If a problem occurs while sending the request or
278   *                         reading the response, or if a timeout occurred
279   *                         while waiting for the response.
280   */
281  @NotNull()
282  private BindResult sendMessageSync(@NotNull final LDAPConnection connection,
283                                     @NotNull final LDAPMessage requestMessage,
284                                     final long timeoutMillis)
285            throws LDAPException
286  {
287    final int msgID = requestMessage.getMessageID();
288    Debug.debugLDAPRequest(Level.INFO, this, msgID, connection);
289
290    final LDAPConnectionLogger logger =
291         connection.getConnectionOptions().getConnectionLogger();
292    if (logger != null)
293    {
294      logger.logBindRequest(connection, messageID, this);
295    }
296
297    final long requestTime = System.nanoTime();
298    connection.getConnectionStatistics().incrementNumBindRequests();
299    connection.sendMessage(requestMessage, timeoutMillis);
300
301    while (true)
302    {
303      final LDAPResponse response = connection.readResponse(messageID);
304      if (response instanceof IntermediateResponse)
305      {
306        final IntermediateResponseListener listener =
307             getIntermediateResponseListener();
308        if (listener != null)
309        {
310          listener.intermediateResponseReturned(
311               (IntermediateResponse) response);
312        }
313      }
314      else
315      {
316        return handleResponse(connection, response, requestTime);
317      }
318    }
319  }
320
321
322
323  /**
324   * Performs the necessary processing for handling a response.
325   *
326   * @param  connection   The connection used to read the response.
327   * @param  response     The response to be processed.
328   * @param  requestTime  The time the request was sent to the server.
329   *
330   * @return  The bind result.
331   *
332   * @throws  LDAPException  If a problem occurs.
333   */
334  @NotNull()
335  private BindResult handleResponse(@NotNull final LDAPConnection connection,
336                                    @Nullable final LDAPResponse response,
337                                    final long requestTime)
338          throws LDAPException
339  {
340    if (response == null)
341    {
342      final long waitTime =
343           StaticUtils.nanosToMillis(System.nanoTime() - requestTime);
344      throw new LDAPException(ResultCode.TIMEOUT,
345           ERR_SASL_BIND_CLIENT_TIMEOUT.get(waitTime, getSASLMechanismName(),
346                messageID, connection.getHostPort()));
347    }
348
349    if (response instanceof ConnectionClosedResponse)
350    {
351      final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
352      final String message = ccr.getMessage();
353      if (message == null)
354      {
355        // The connection was closed while waiting for the response.
356        throw new LDAPException(ccr.getResultCode(),
357             ERR_CONN_CLOSED_WAITING_FOR_BIND_RESPONSE.get(
358                  connection.getHostPort(), toString()));
359      }
360      else
361      {
362        // The connection was closed while waiting for the response.
363        throw new LDAPException(ccr.getResultCode(),
364             ERR_CONN_CLOSED_WAITING_FOR_BIND_RESPONSE_WITH_MESSAGE.get(
365                  connection.getHostPort(), toString(), message));
366      }
367    }
368
369    connection.getConnectionStatistics().incrementNumBindResponses(
370         System.nanoTime() - requestTime);
371    return (BindResult) response;
372  }
373
374
375
376  /**
377   * {@inheritDoc}
378   */
379  @InternalUseOnly()
380  @Override()
381  public final void responseReceived(@NotNull final LDAPResponse response)
382         throws LDAPException
383  {
384    try
385    {
386      responseQueue.put(response);
387    }
388    catch (final Exception e)
389    {
390      Debug.debugException(e);
391
392      if (e instanceof InterruptedException)
393      {
394        Thread.currentThread().interrupt();
395      }
396
397      throw new LDAPException(ResultCode.LOCAL_ERROR,
398           ERR_EXCEPTION_HANDLING_RESPONSE.get(
399                StaticUtils.getExceptionMessage(e)),
400           e);
401    }
402  }
403
404
405
406  /**
407   * {@inheritDoc}
408   */
409  @Override()
410  public void toCode(@NotNull final List<String> lineList,
411                     @NotNull final String requestID,
412                     final int indentSpaces, final boolean includeProcessing)
413  {
414    // Create the request variable.
415    final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(4);
416    constructorArgs.add(ToCodeArgHelper.createString(null, "Bind DN"));
417    constructorArgs.add(ToCodeArgHelper.createString(getSASLMechanismName(),
418         "SASL Mechanism Name"));
419    constructorArgs.add(ToCodeArgHelper.createByteArray(
420         "---redacted-SASL-credentials".getBytes(StandardCharsets.UTF_8), true,
421         "SASL Credentials"));
422
423    final Control[] controls = getControls();
424    if (controls.length > 0)
425    {
426      constructorArgs.add(ToCodeArgHelper.createControlArray(controls,
427           "Bind Controls"));
428    }
429
430    ToCodeHelper.generateMethodCall(lineList, indentSpaces,
431         "GenericSASLBindRequest", requestID + "Request",
432         "new GenericSASLBindRequest", constructorArgs);
433
434
435    // Add lines for processing the request and obtaining the result.
436    if (includeProcessing)
437    {
438      // Generate a string with the appropriate indent.
439      final StringBuilder buffer = new StringBuilder();
440      for (int i=0; i < indentSpaces; i++)
441      {
442        buffer.append(' ');
443      }
444      final String indent = buffer.toString();
445
446      lineList.add("");
447      lineList.add(indent + '{');
448      lineList.add(indent + "  BindResult " + requestID +
449           "Result = connection.bind(" + requestID + "Request);");
450      lineList.add(indent + "  // The bind was processed successfully.");
451      lineList.add(indent + '}');
452      lineList.add(indent + "catch (SASLBindInProgressException e)");
453      lineList.add(indent + '{');
454      lineList.add(indent + "  // The SASL bind requires multiple stages.  " +
455           "Continue it here.");
456      lineList.add(indent + "  // Do not attempt to use the connection for " +
457           "any other purpose until bind processing has completed.");
458      lineList.add(indent + '}');
459      lineList.add(indent + "catch (LDAPException e)");
460      lineList.add(indent + '{');
461      lineList.add(indent + "  // The bind failed.  Maybe the following will " +
462           "help explain why.");
463      lineList.add(indent + "  // Note that the connection is now likely in " +
464           "an unauthenticated state.");
465      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
466      lineList.add(indent + "  String message = e.getMessage();");
467      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
468      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
469      lineList.add(indent + "  Control[] responseControls = " +
470           "e.getResponseControls();");
471      lineList.add(indent + '}');
472    }
473  }
474}