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