From b4e7c2adbcf17bb3017ffc4aad6d395187bc4e59 Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Sun, 29 Jul 2007 20:58:29 +0000
Subject: [PATCH] Add support for a new disconnect client task that can be used to allow an administrator to terminate a client connection if the need arises.  The requester must have the disconnect-client privilege.  The task entry should contain the ds-task-disconnect object class, which requires the ds-task-disconnect-connection-id attribute type and optionally allows the ds-task-disconnect-notify-client and ds-task-disconnect-message attribute types.

---
 opends/resource/schema/02-config.ldif                                                               |   15 +
 opends/tests/unit-tests-testng/src/server/org/opends/server/tasks/DisconnectClientTaskTestCase.java |  271 +++++++++++++++++++
 opends/src/server/org/opends/server/extensions/GetConnectionIDExtendedOperation.java                |  139 +++++++++
 opends/src/server/org/opends/server/messages/TaskMessages.java                                      |   90 ++++++
 opends/src/server/org/opends/server/tasks/DisconnectClientTask.java                                 |  236 ++++++++++++++++
 opends/resource/config/config.ldif                                                                  |    7 
 opends/src/server/org/opends/server/util/ServerConstants.java                                       |   35 ++
 7 files changed, 793 insertions(+), 0 deletions(-)

diff --git a/opends/resource/config/config.ldif b/opends/resource/config/config.ldif
index a62d9ff..5998b95 100644
--- a/opends/resource/config/config.ldif
+++ b/opends/resource/config/config.ldif
@@ -405,6 +405,13 @@
 ds-cfg-extended-operation-handler-class: org.opends.server.extensions.CancelExtendedOperation
 ds-cfg-extended-operation-handler-enabled: true
 
+dn: cn=Get Connection ID,cn=Extended Operations,cn=config
+objectClass: top
+objectClass: ds-cfg-extended-operation-handler
+cn: Cancel
+ds-cfg-extended-operation-handler-class: org.opends.server.extensions.GetConnectionIDExtendedOperation
+ds-cfg-extended-operation-handler-enabled: true
+
 dn: cn=Password Modify,cn=Extended Operations,cn=config
 objectClass: top
 objectClass: ds-cfg-extended-operation-handler
diff --git a/opends/resource/schema/02-config.ldif b/opends/resource/schema/02-config.ldif
index 174bc97..e67d9eb 100644
--- a/opends/resource/schema/02-config.ldif
+++ b/opends/resource/schema/02-config.ldif
@@ -1520,6 +1520,16 @@
   NAME 'ds-task-import-clear-backend'
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE
   X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.453
+  NAME 'ds-task-disconnect-connection-id'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
+  X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.454 NAME 'ds-task-disconnect-message'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
+  X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.455
+  NAME 'ds-task-disconnect-notify-client' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
   NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL
   MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled )
@@ -2138,4 +2148,9 @@
   MUST ( ds-cfg-sender-address $ ds-cfg-recipient-address $
   ds-cfg-message-subject $ ds-cfg-message-body )
   X-ORIGIN 'OpenDS Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.119
+  NAME 'ds-task-disconnect' SUP ds-task STRUCTURAL
+  MUST ds-task-disconnect-connection-id
+  MAY ( ds-task-disconnect-message $ ds-task-disconnect-notify-client )
+  X-ORIGIN 'OpenDS Directory Server' )
 
diff --git a/opends/src/server/org/opends/server/extensions/GetConnectionIDExtendedOperation.java b/opends/src/server/org/opends/server/extensions/GetConnectionIDExtendedOperation.java
new file mode 100644
index 0000000..d1de649
--- /dev/null
+++ b/opends/src/server/org/opends/server/extensions/GetConnectionIDExtendedOperation.java
@@ -0,0 +1,139 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.extensions;
+
+
+
+import org.opends.server.admin.std.server.ExtendedOperationHandlerCfg;
+import org.opends.server.api.ExtendedOperationHandler;
+import org.opends.server.config.ConfigException;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ExtendedOperation;
+import org.opends.server.protocols.asn1.ASN1Exception;
+import org.opends.server.protocols.asn1.ASN1Long;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.ResultCode;
+
+import static org.opends.server.messages.ExtensionsMessages.*;
+import static org.opends.server.messages.MessageHandler.*;
+import static org.opends.server.util.ServerConstants.*;
+
+
+/**
+ * This class implements the "Get Connection ID" extended operation that can be
+ * used to get the connection ID of the associated client connection.
+ */
+public class GetConnectionIDExtendedOperation
+       extends ExtendedOperationHandler<ExtendedOperationHandlerCfg>
+{
+  /**
+   * Create an instance of this "Get Connection ID" extended operation.  All
+   * initialization should be performed in the
+   * {@code initializeExtendedOperationHandler} method.
+   */
+  public GetConnectionIDExtendedOperation()
+  {
+    super();
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void initializeExtendedOperationHandler(
+       ExtendedOperationHandlerCfg config)
+       throws ConfigException, InitializationException
+  {
+    // No special configuration is required.
+
+    DirectoryServer.registerSupportedExtension(OID_GET_CONNECTION_ID_EXTOP,
+                                               this);
+
+    registerControlsAndFeatures();
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void finalizeExtendedOperationHandler()
+  {
+    DirectoryServer.deregisterSupportedExtension(OID_WHO_AM_I_REQUEST);
+
+    deregisterControlsAndFeatures();
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void processExtendedOperation(ExtendedOperation operation)
+  {
+    operation.setResponseOID(OID_GET_CONNECTION_ID_EXTOP);
+    operation.setResponseValue(
+         encodeResponseValue(operation.getConnectionID()));
+    operation.setResultCode(ResultCode.SUCCESS);
+  }
+
+
+
+  /**
+   * Encodes the provided connection ID in an octet string suitable for use as
+   * the value for this extended operation.
+   *
+   * @param  connectionID  The connection ID to be encoded.
+   *
+   * @return  The ASN.1 octet string containing the encoded connection ID.
+   */
+  public static ASN1OctetString encodeResponseValue(long connectionID)
+  {
+    return new ASN1OctetString(new ASN1Long(connectionID).encode());
+  }
+
+
+
+  /**
+   * Decodes the provided ASN.1 octet string to extract the connection ID.
+   *
+   * @param  responseValue  The response value to be decoded.
+   *
+   * @return  The connection ID decoded from the provided response value.
+   *
+   * @throws  ASN1Exception  If an error occurs while trying to decode the
+   *                         response value.
+   */
+  public static long decodeResponseValue(ASN1OctetString responseValue)
+         throws ASN1Exception
+  {
+    return ASN1Long.decodeAsLong(responseValue.value()).longValue();
+  }
+}
+
diff --git a/opends/src/server/org/opends/server/messages/TaskMessages.java b/opends/src/server/org/opends/server/messages/TaskMessages.java
index 9828f8c..364ee2d 100644
--- a/opends/src/server/org/opends/server/messages/TaskMessages.java
+++ b/opends/src/server/org/opends/server/messages/TaskMessages.java
@@ -274,6 +274,76 @@
 
 
   /**
+   * The message ID for the message that will be used if the client does not
+   * have the DISCONNECT_CLIENT privilege.  It does not take any arguments.
+   */
+  public static final int  MSGID_TASK_DISCONNECT_NO_PRIVILEGE =
+       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 25;
+
+
+
+  /**
+   * The message ID for the message that will be used if the provided connection
+   * ID cannot be decoded.  This takes a single argument, which is the invalid
+   * value.
+   */
+  public static final int  MSGID_TASK_DISCONNECT_INVALID_CONN_ID =
+       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 26;
+
+
+
+  /**
+   * The message ID for the message that will be used if the task entry does
+   * not specify a target connection ID.  This takes a single argument, which is
+   * the name of the attribute type used to specify the target connection ID.
+   */
+  public static final int  MSGID_TASK_DISCONNECT_NO_CONN_ID =
+       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 27;
+
+
+
+  /**
+   * The message ID for the message that will be used if the notifyClient
+   * attribute value cannot be decoded.  This takes a single argument, which is
+   * the invalid value.
+   */
+  public static final int  MSGID_TASK_DISCONNECT_INVALID_NOTIFY_CLIENT =
+       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 28;
+
+
+
+  /**
+   * The message ID for the message that will be used as a generic message that
+   * may be sent to the client if no other value is given.  It does not take
+   * any arguments.
+   */
+  public static final int  MSGID_TASK_DISCONNECT_GENERIC_MESSAGE =
+       CATEGORY_MASK_TASK | SEVERITY_MASK_INFORMATIONAL | 29;
+
+
+
+  /**
+   * The message ID for the message that will be used if no client connection
+   * can be found with the specified connection ID.  It takes a single argument,
+   * which is the target connection ID.
+   */
+  public static final int  MSGID_TASK_DISCONNECT_NO_SUCH_CONNECTION =
+       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 30;
+
+
+
+  /**
+   * The message ID for the message that will be used as the message ID for the
+   * disconnectClient method.  The associated message will be defined, but it
+   * will not actually be used, since only the message ID is needed.  It does
+   * not take any arguments.
+   */
+  public static final int  MSGID_TASK_DISCONNECT_MESSAGE =
+       CATEGORY_MASK_TASK | SEVERITY_MASK_INFORMATIONAL | 31;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -362,6 +432,26 @@
     registerMessage(MSGID_TASK_LEAVELOCKDOWN_NOT_LOOPBACK,
                     "Only root users connected from a loopback address may " +
                     "cause the server to leave lockdown mode");
+
+
+    registerMessage(MSGID_TASK_DISCONNECT_NO_PRIVILEGE,
+                    "You do not have sufficient privileges to terminate " +
+                    "client connections");
+    registerMessage(MSGID_TASK_DISCONNECT_INVALID_CONN_ID,
+                    "Unable to decode value %s as an integer connection ID");
+    registerMessage(MSGID_TASK_DISCONNECT_NO_CONN_ID,
+                    "Attribute %s must be provided to specify the connection " +
+                    "ID for the client to disconnect");
+    registerMessage(MSGID_TASK_DISCONNECT_INVALID_NOTIFY_CLIENT,
+                    "Unable to decode value %s as an indication of whether " +
+                    "to notify the client before disconnecting it.  The " +
+                    "provided value should be either 'true' or 'false'");
+    registerMessage(MSGID_TASK_DISCONNECT_GENERIC_MESSAGE,
+                    "An administrator has terminated this client connection");
+    registerMessage(MSGID_TASK_DISCONNECT_NO_SUCH_CONNECTION,
+                    "There is no client connection with connection ID %s");
+    registerMessage(MSGID_TASK_DISCONNECT_MESSAGE,
+                    "An administrator has terminated this client connection");
   }
 }
 
diff --git a/opends/src/server/org/opends/server/tasks/DisconnectClientTask.java b/opends/src/server/org/opends/server/tasks/DisconnectClientTask.java
new file mode 100644
index 0000000..adaf290
--- /dev/null
+++ b/opends/src/server/org/opends/server/tasks/DisconnectClientTask.java
@@ -0,0 +1,236 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.tasks;
+
+
+
+import java.util.List;
+
+import org.opends.server.admin.std.server.ConnectionHandlerCfg;
+import org.opends.server.backends.task.Task;
+import org.opends.server.backends.task.TaskState;
+import org.opends.server.api.ClientConnection;
+import org.opends.server.api.ConnectionHandler;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.DisconnectReason;
+import org.opends.server.types.Entry;
+import org.opends.server.types.ErrorLogCategory;
+import org.opends.server.types.ErrorLogSeverity;
+import org.opends.server.types.Operation;
+import org.opends.server.types.Privilege;
+import org.opends.server.types.ResultCode;
+
+import static org.opends.server.messages.MessageHandler.*;
+import static org.opends.server.messages.TaskMessages.*;
+import static org.opends.server.util.ServerConstants.*;
+import static org.opends.server.util.StaticUtils.*;
+
+
+
+/**
+ * This class provides an implementation of a Directory Server task that can be
+ * used to terminate a client connection.
+ */
+public class DisconnectClientTask
+       extends Task
+{
+  // Indicates whether to send a notification message to the client.
+  private boolean notifyClient;
+
+  // The connection ID for the client connection to terminate.
+  private long connectionID;
+
+  // The disconnect message to send to the client.
+  private String disconnectMessage;
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void initializeTask()
+         throws DirectoryException
+  {
+    // If the client connection is available, then make sure the client has the
+    // DISCONNECT_CLIENT privilege.
+    Operation operation = getOperation();
+    if (operation != null)
+    {
+      ClientConnection conn = operation.getClientConnection();
+      if (! conn.hasPrivilege(Privilege.DISCONNECT_CLIENT, operation))
+      {
+        int    msgID   = MSGID_TASK_DISCONNECT_NO_PRIVILEGE;
+        String message = getMessage(msgID);
+        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
+                                     message, msgID);
+      }
+    }
+
+
+    // Get the connection ID for the client connection.
+    Entry taskEntry = getTaskEntry();
+    connectionID = -1L;
+    AttributeType attrType =
+         DirectoryServer.getAttributeType(ATTR_TASK_DISCONNECT_CONN_ID, true);
+    List<Attribute> attrList = taskEntry.getAttribute(attrType);
+    if (attrList != null)
+    {
+connIDLoop:
+      for (Attribute a : attrList)
+      {
+        for (AttributeValue v : a.getValues())
+        {
+          try
+          {
+            connectionID = Long.parseLong(v.getStringValue());
+            break connIDLoop;
+          }
+          catch (Exception e)
+          {
+            int    msgID   = MSGID_TASK_DISCONNECT_INVALID_CONN_ID;
+            String message = getMessage(msgID, v.getStringValue());
+            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
+                                         message, msgID, e);
+          }
+        }
+      }
+    }
+
+    if (connectionID < 0)
+    {
+      int    msgID   = MSGID_TASK_DISCONNECT_NO_CONN_ID;
+      String message = getMessage(msgID, ATTR_TASK_DISCONNECT_CONN_ID);
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+                                   message, msgID);
+    }
+
+
+    // Determine whether to notify the client.
+    notifyClient = false;
+    attrType =
+         DirectoryServer.getAttributeType(ATTR_TASK_DISCONNECT_NOTIFY_CLIENT,
+                                          true);
+    attrList = taskEntry.getAttribute(attrType);
+    if (attrList != null)
+    {
+notifyClientLoop:
+      for (Attribute a : attrList)
+      {
+        for (AttributeValue v : a.getValues())
+        {
+          String stringValue = toLowerCase(v.getStringValue());
+          if (stringValue.equals("true"))
+          {
+            notifyClient = true;
+            break notifyClientLoop;
+          }
+          else if (stringValue.equals("false"))
+          {
+            break notifyClientLoop;
+          }
+          else
+          {
+            int    msgID   = MSGID_TASK_DISCONNECT_INVALID_NOTIFY_CLIENT;
+            String message = getMessage(msgID, stringValue);
+            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
+                                         message, msgID);
+          }
+        }
+      }
+    }
+
+
+    // Get the disconnect message.
+    disconnectMessage = getMessage(MSGID_TASK_DISCONNECT_GENERIC_MESSAGE);
+    attrType = DirectoryServer.getAttributeType(ATTR_TASK_DISCONNECT_MESSAGE,
+                                                true);
+    attrList = taskEntry.getAttribute(attrType);
+    if (attrList != null)
+    {
+disconnectMessageLoop:
+      for (Attribute a : attrList)
+      {
+        for (AttributeValue v : a.getValues())
+        {
+          disconnectMessage = v.getStringValue();
+          break disconnectMessageLoop;
+        }
+      }
+    }
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  protected TaskState runTask()
+  {
+    // Get the specified client connection.
+    ClientConnection clientConnection = null;
+    for (ConnectionHandler handler : DirectoryServer.getConnectionHandlers())
+    {
+      ConnectionHandler<? extends ConnectionHandlerCfg> connHandler =
+           (ConnectionHandler<? extends ConnectionHandlerCfg>) handler;
+      for (ClientConnection c : connHandler.getClientConnections())
+      {
+        if (c.getConnectionID() == connectionID)
+        {
+          clientConnection = c;
+          break;
+        }
+      }
+    }
+
+
+    // If there is no such client connection, then return an error.  Otherwise,
+    // terminate it.
+    if (clientConnection == null)
+    {
+      int    msgID   = MSGID_TASK_DISCONNECT_NO_SUCH_CONNECTION;
+      String message = getMessage(msgID, connectionID);
+
+      logError(ErrorLogCategory.TASK, ErrorLogSeverity.SEVERE_ERROR, message,
+               msgID);
+
+      return TaskState.COMPLETED_WITH_ERRORS;
+    }
+    else
+    {
+      clientConnection.disconnect(DisconnectReason.ADMIN_DISCONNECT,
+                                  notifyClient, disconnectMessage,
+                                  MSGID_TASK_DISCONNECT_MESSAGE);
+      return TaskState.COMPLETED_SUCCESSFULLY;
+    }
+  }
+}
+
diff --git a/opends/src/server/org/opends/server/util/ServerConstants.java b/opends/src/server/org/opends/server/util/ServerConstants.java
index dd213c4..6a54295 100644
--- a/opends/src/server/org/opends/server/util/ServerConstants.java
+++ b/opends/src/server/org/opends/server/util/ServerConstants.java
@@ -498,6 +498,32 @@
 
 
   /**
+   * The name of the attribute that is used to specify the connection ID of the
+   * connection to disconnect.
+   */
+  public static final String ATTR_TASK_DISCONNECT_CONN_ID =
+       "ds-task-disconnect-connection-id";
+
+
+
+  /**
+   * The name of the attribute that is used to specify the disconnect message.
+   */
+  public static final String ATTR_TASK_DISCONNECT_MESSAGE =
+       "ds-task-disconnect-message";
+
+
+
+  /**
+   * The name of the attribute that is used to indicate whether to notify the
+   * connection it is about to be terminated.
+   */
+  public static final String ATTR_TASK_DISCONNECT_NOTIFY_CLIENT =
+       "ds-task-disconnect-notify-client";
+
+
+
+  /**
    * The name of the attribute that is used to specify the total number of
    * connections established since startup, formatted in camel case.
    */
@@ -687,6 +713,15 @@
 
 
   /**
+   * The OID for the extended operation that can be used to get the client
+   * connection ID.  It will be both the request and response OID.
+   */
+  public static final String OID_GET_CONNECTION_ID_EXTOP =
+       "1.3.6.1.4.1.26027.1.6.2";
+
+
+
+  /**
    * The request OID for the password modify extended operation.
    */
   public static final String OID_PASSWORD_MODIFY_REQUEST =
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/tasks/DisconnectClientTaskTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/tasks/DisconnectClientTaskTestCase.java
new file mode 100644
index 0000000..1efb90b
--- /dev/null
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/tasks/DisconnectClientTaskTestCase.java
@@ -0,0 +1,271 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.tasks;
+
+
+
+import java.net.Socket;
+
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeClass;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.backends.task.Task;
+import org.opends.server.backends.task.TaskBackend;
+import org.opends.server.backends.task.TaskState;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.extensions.GetConnectionIDExtendedOperation;
+import org.opends.server.protocols.asn1.*;
+import org.opends.server.protocols.ldap.*;
+import org.opends.server.types.DN;
+
+import static org.testng.Assert.*;
+
+import static org.opends.server.util.ServerConstants.*;
+
+
+
+/**
+ * Tests the disconnect client task.
+ */
+public class DisconnectClientTaskTestCase
+       extends TasksTestCase
+{
+  /**
+   * Make sure that the Directory Server is running.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void startServer()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+
+
+
+  /**
+   * Tests the ability of the server to disconnect an arbitrary client
+   * connection with a notice of disconnection.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testDisconnectWithNotification()
+         throws Exception
+  {
+    // Establish a connection to the server, bind, and get the connection ID.
+    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    BindRequestProtocolOp bindRequest =
+         new BindRequestProtocolOp(new ASN1OctetString("cn=Directory Manager"),
+                                   3, new ASN1OctetString("password"));
+    LDAPMessage message = new LDAPMessage(1, bindRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+    assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);
+
+
+    ExtendedRequestProtocolOp extendedRequest =
+         new ExtendedRequestProtocolOp(OID_GET_CONNECTION_ID_EXTOP);
+    message = new LDAPMessage(2, extendedRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    ExtendedResponseProtocolOp extendedResponse =
+         message.getExtendedResponseProtocolOp();
+    assertEquals(extendedResponse.getResultCode(), LDAPResultCode.SUCCESS);
+    assertEquals(extendedResponse.getOID(), OID_GET_CONNECTION_ID_EXTOP);
+    long connectionID = GetConnectionIDExtendedOperation.decodeResponseValue(
+                             extendedResponse.getValue());
+
+
+    // Invoke the disconnect client task.
+    String taskID = "Disconnect Client " + connectionID;
+    String disconnectMessage = "testDisconnectWithNotification";
+    DN taskDN = DN.decode("ds-task-id=" + taskID +
+                          ",cn=Scheduled Tasks,cn=Tasks");
+    TestCaseUtils.addEntry(
+      "dn: " + taskDN.toString(),
+      "objectClass: top",
+      "objectClass: ds-task",
+      "objectClass: ds-task-disconnect",
+      "ds-task-id: " + taskID,
+      "ds-task-class-name: org.opends.server.tasks.DisconnectClientTask",
+      "ds-task-disconnect-connection-id: " + connectionID,
+      "ds-task-disconnect-notify-client: true",
+      "ds-task-disconnect-message: " + disconnectMessage);
+
+    Task task = getCompletedTask(taskDN);
+    assertNotNull(task);
+    assertEquals(task.getTaskState(), TaskState.COMPLETED_SUCCESSFULLY);
+
+
+    // Make sure that we get a notice of disconnection on the initial
+    // connection.
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    extendedResponse = message.getExtendedResponseProtocolOp();
+    assertEquals(extendedResponse.getOID(),
+                 LDAPConstants.OID_NOTICE_OF_DISCONNECTION);
+    assertEquals(extendedResponse.getErrorMessage(), disconnectMessage);
+
+    try
+    {
+      s.close();
+    } catch (Exception e) {}
+  }
+
+
+
+  /**
+   * Tests the ability of the server to disconnect an arbitrary client
+   * connection without a notice of disconnection.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testDisconnectWithoutNotification()
+         throws Exception
+  {
+    // Establish a connection to the server, bind, and get the connection ID.
+    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    BindRequestProtocolOp bindRequest =
+         new BindRequestProtocolOp(new ASN1OctetString("cn=Directory Manager"),
+                                   3, new ASN1OctetString("password"));
+    LDAPMessage message = new LDAPMessage(1, bindRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+    assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);
+
+
+    ExtendedRequestProtocolOp extendedRequest =
+         new ExtendedRequestProtocolOp(OID_GET_CONNECTION_ID_EXTOP);
+    message = new LDAPMessage(2, extendedRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    ExtendedResponseProtocolOp extendedResponse =
+         message.getExtendedResponseProtocolOp();
+    assertEquals(extendedResponse.getResultCode(), LDAPResultCode.SUCCESS);
+    assertEquals(extendedResponse.getOID(), OID_GET_CONNECTION_ID_EXTOP);
+    long connectionID = GetConnectionIDExtendedOperation.decodeResponseValue(
+                             extendedResponse.getValue());
+
+
+    // Invoke the disconnect client task.
+    String taskID = "Disconnect Client " + connectionID;
+    DN taskDN = DN.decode("ds-task-id=" + taskID +
+                          ",cn=Scheduled Tasks,cn=Tasks");
+    TestCaseUtils.addEntry(
+      "dn: " + taskDN.toString(),
+      "objectClass: top",
+      "objectClass: ds-task",
+      "objectClass: ds-task-disconnect",
+      "ds-task-id: " + taskID,
+      "ds-task-class-name: org.opends.server.tasks.DisconnectClientTask",
+      "ds-task-disconnect-connection-id: " + connectionID,
+      "ds-task-disconnect-notify-client: false");
+
+    Task task = getCompletedTask(taskDN);
+    assertNotNull(task);
+    assertEquals(task.getTaskState(), TaskState.COMPLETED_SUCCESSFULLY);
+
+
+    // Make sure that the client connection has been closed with no notice of
+    // disconnection.
+    assertNull(r.readElement());
+
+    try
+    {
+      s.close();
+    } catch (Exception e) {}
+  }
+
+
+
+  /**
+   * Retrieves the specified task from the server, waiting for it to finish all
+   * the running its going to do before returning.
+   *
+   * @param  taskEntryDN  The DN of the entry for the task to retrieve.
+   *
+   * @return  The requested task entry.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private Task getCompletedTask(DN taskEntryDN)
+          throws Exception
+  {
+    TaskBackend taskBackend =
+         (TaskBackend) DirectoryServer.getBackend(DN.decode("cn=tasks"));
+    Task task = taskBackend.getScheduledTask(taskEntryDN);
+    if (task == null)
+    {
+      long stopWaitingTime = System.currentTimeMillis() + 10000L;
+      while ((task == null) && (System.currentTimeMillis() < stopWaitingTime))
+      {
+        Thread.sleep(10);
+        task = taskBackend.getScheduledTask(taskEntryDN);
+      }
+    }
+
+    if (task == null)
+    {
+      throw new AssertionError("There is no such task " +
+                               taskEntryDN.toString());
+    }
+
+    if (! TaskState.isDone(task.getTaskState()))
+    {
+      long stopWaitingTime = System.currentTimeMillis() + 20000L;
+      while ((! TaskState.isDone(task.getTaskState())) &&
+             (System.currentTimeMillis() < stopWaitingTime))
+      {
+        Thread.sleep(10);
+      }
+    }
+
+    if (! TaskState.isDone(task.getTaskState()))
+    {
+      throw new AssertionError("Task " + taskEntryDN.toString() +
+                               " did not complete in a timely manner.");
+    }
+
+    return task;
+  }
+}
+

--
Gitblit v1.10.0