/*
* 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
*
*
* Copyright 2013 ForgeRock AS
*/
package org.opends.server.protocols.http;
import static org.forgerock.opendj.adapter.server2x.Converters.*;
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.loggers.AccessLogger.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.servlet.http.HttpServletRequest;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.ResultHandler;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.responses.Result;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import org.opends.server.api.ClientConnection;
import org.opends.server.core.AddOperation;
import org.opends.server.core.BindOperation;
import org.opends.server.core.CompareOperation;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ExtendedOperation;
import org.opends.server.core.ModifyDNOperation;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.SearchOperation;
import org.opends.server.loggers.HTTPAccessLogger;
import org.opends.server.loggers.HTTPRequestInfo;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.ldap.AddResponseProtocolOp;
import org.opends.server.protocols.ldap.BindResponseProtocolOp;
import org.opends.server.protocols.ldap.CompareResponseProtocolOp;
import org.opends.server.protocols.ldap.DeleteResponseProtocolOp;
import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp;
import org.opends.server.protocols.ldap.LDAPMessage;
import org.opends.server.protocols.ldap.ModifyDNResponseProtocolOp;
import org.opends.server.protocols.ldap.ModifyResponseProtocolOp;
import org.opends.server.protocols.ldap.ProtocolOp;
import org.opends.server.protocols.ldap.SearchResultDoneProtocolOp;
import org.opends.server.protocols.ldap.SearchResultEntryProtocolOp;
import org.opends.server.protocols.ldap.SearchResultReferenceProtocolOp;
import org.opends.server.types.CancelRequest;
import org.opends.server.types.CancelResult;
import org.opends.server.types.DN;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DisconnectReason;
import org.opends.server.types.IntermediateResponse;
import org.opends.server.types.Operation;
import org.opends.server.types.OperationType;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
import com.forgerock.opendj.util.AsynchronousFutureResult;
/**
* This class defines an HTTP client connection, which is a type of client
* connection that will be accepted by an instance of the HTTP connection
* handler.
*/
final class HTTPClientConnection extends ClientConnection implements
HTTPRequestInfo
{
// TODO JNR Confirm with Matt that persistent searches are inapplicable to
// Rest2LDAP.
// TODO JNR Should I override getIdleTime()?
/**
* Class grouping together an {@link Operation} and its associated
* {@link AsynchronousFutureResult} to ensure they are both atomically added
* and removed from the {@link HTTPClientConnection#operationsInProgress} Map.
*/
private static final class OperationWithFutureResult
{
final Operation operation;
final AsynchronousFutureResult<Result, ResultHandler<? super Result>>
futureResult;
public OperationWithFutureResult(Operation operation,
AsynchronousFutureResult<Result, ResultHandler<? super Result>>
futureResult)
{
this.operation = operation;
this.futureResult = futureResult;
}
@Override
public String toString()
{
return operation.toString();
}
}
/** The tracer object for the debug logger. */
private static final DebugTracer TRACER = getTracer();
/**
* Official servlet property giving access to the SSF (Security Strength
* Factor) used to encrypt the current connection.
*/
private static final String SERVLET_SSF_CONSTANT =
"javax.servlet.request.key_size";
/**
* Indicates whether the Directory Server believes this connection to be valid
* and available for communication.
*/
private volatile boolean connectionValid = true;
/**
* Indicates whether this connection is about to be closed. This will be used
* to prevent accepting new requests while a disconnect is in progress.
*/
private boolean disconnectRequested;
/**
* Indicates whether the connection should keep statistics regarding the
* operations that it is performing.
*/
private final boolean keepStats;
/**
* The Map (messageID => {@link OperationWithFutureResult}) of all operations
* currently in progress on this connection.
*/
private final Map<Integer, OperationWithFutureResult> operationsInProgress =
new ConcurrentHashMap<Integer, OperationWithFutureResult>();
/**
* The number of operations performed on this connection. Used to compare with
* the resource limits of the network group.
*/
private final AtomicLong operationsPerformed = new AtomicLong(0);
/**
* The lock used to provide threadsafe access to the map of operations in
* progress. This is used when we want to prevent puts on this map while we
* are removing all operations in progress.
*/
private final Object opsInProgressLock = new Object();
/** The connection ID assigned to this connection. */
private final long connectionID;
/** The reference to the connection handler that accepted this connection. */
private final HTTPConnectionHandler connectionHandler;
/** The statistics tracker associated with this client connection. */
private final HTTPStatistics statTracker;
private boolean useNanoTime = false;
/** Total execution time for this request. */
private AtomicLong totalProcessingTime = new AtomicLong();
/** The protocol in use for this client connection. */
private String protocol;
/** The HTTP method/verb used for this request. */
private final String method;
/** The query issued by the client. */
private final String query;
/** The user agent used by the client. */
private final String userAgent;
/** The username that was used to authenticate. */
private String authUser;
/**
* The HTTP status code returned to the client. Using 0 to say no status code
* was set since it is not .
*/
private AtomicInteger statusCode = new AtomicInteger(0);
/** The client (remote) address. */
private String clientAddress;
/** The client (remote) host name. */
private String clientHost;
/** The client (remote) port. */
private int clientPort;
/** The remote (client) address. */
private InetAddress remoteAddress;
/** The server (local) address. */
private String serverAddress;
/** The server (local) host name. */
private String serverHost;
/** The server (local) port. */
private int serverPort;
/** The local (server) address. */
private InetAddress localAddress;
/** Whether this connection is secure. */
private boolean isSecure;
/** Security-Strength Factor extracted from the request attribute. */
private int securityStrengthFactor;
/**
* Constructs an instance of this class.
*
* @param connectionHandler
* the connection handler that accepted this connection
* @param request
* represents this client connection.
*/
public HTTPClientConnection(HTTPConnectionHandler connectionHandler,
HttpServletRequest request)
{
this.connectionHandler = connectionHandler;
// memoize all the fields we need from the request before Grizzly decides to
// recycle it
this.clientAddress = request.getRemoteAddr();
this.clientPort = request.getRemotePort();
this.serverAddress = request.getLocalAddr();
this.serverPort = request.getLocalPort();
this.remoteAddress = toInetAddress(request.getRemoteAddr());
this.localAddress = toInetAddress(request.getLocalAddr());
this.isSecure = request.isSecure();
this.securityStrengthFactor =
calcSSF(request.getAttribute(SERVLET_SSF_CONSTANT));
this.method = request.getMethod();
this.query = computeQuery(request);
this.protocol = request.getProtocol();
this.userAgent = request.getHeader("User-Agent");
this.statTracker = this.connectionHandler.getStatTracker();
this.keepStats = connectionHandler.keepStats();
if (this.keepStats)
{
this.statTracker.updateConnect();
this.useNanoTime = DirectoryServer.getUseNanoTime();
}
this.connectionID = DirectoryServer.newConnectionAccepted(this);
}
private String computeQuery(HttpServletRequest request)
{
if (request.getQueryString() != null)
{
return request.getRequestURI() + "?" + request.getQueryString();
}
return request.getRequestURI();
}
/** {@inheritDoc} */
@Override
public String getAuthUser()
{
return this.authUser;
}
/** {@inheritDoc} */
@Override
public long getConnectionID()
{
return connectionID;
}
/** {@inheritDoc} */
@Override
public HTTPConnectionHandler getConnectionHandler()
{
return connectionHandler;
}
/** {@inheritDoc} */
@Override
public long getTotalProcessingTime()
{
return totalProcessingTime.get();
}
/** {@inheritDoc} */
@Override
public String getProtocol()
{
return protocol;
}
/** {@inheritDoc} */
@Override
public String getClientAddress()
{
return clientAddress;
}
/** {@inheritDoc} */
@Override
public String getClientHost()
{
return clientHost;
}
/** {@inheritDoc} */
@Override
public int getClientPort()
{
return clientPort;
}
/** {@inheritDoc} */
@Override
public String getServerAddress()
{
return serverAddress;
}
/** {@inheritDoc} */
@Override
public String getServerHost()
{
return serverHost;
}
/** {@inheritDoc} */
@Override
public int getServerPort()
{
return serverPort;
}
/** {@inheritDoc} */
@Override
public InetAddress getRemoteAddress()
{
return remoteAddress;
}
/** {@inheritDoc} */
@Override
public InetAddress getLocalAddress()
{
return localAddress;
}
/** {@inheritDoc} */
@Override
public boolean isSecure()
{
return isSecure;
}
/** {@inheritDoc} */
@Override
public void sendResponse(Operation operation)
{
final long time = getProcessingTime(operation);
this.totalProcessingTime.addAndGet(time);
if (keepStats)
{
this.statTracker.updateRequestMonitoringData(getMethod(), time);
this.statTracker.updateOperationMonitoringData(operation
.getOperationType(), time);
}
OperationWithFutureResult op =
this.operationsInProgress.get(operation.getMessageID());
if (op != null)
{
try
{
op.futureResult.handleResult(getResponseResult(operation));
if (keepStats)
{
this.statTracker.updateMessageWritten(new LDAPMessage(operation
.getMessageID(), toResponseProtocolOp(operation)));
}
}
catch (ErrorResultException e)
{
op.futureResult.handleErrorResult(e);
}
}
}
private long getProcessingTime(Operation operation)
{
if (useNanoTime)
{
return operation.getProcessingNanoTime();
}
return operation.getProcessingTime();
}
private ProtocolOp toResponseProtocolOp(Operation operation)
{
final int resultCode = operation.getResultCode().getIntValue();
if (operation instanceof AddOperation)
{
return new AddResponseProtocolOp(resultCode);
}
else if (operation instanceof BindOperation)
{
return new BindResponseProtocolOp(resultCode);
}
else if (operation instanceof CompareOperation)
{
return new CompareResponseProtocolOp(resultCode);
}
else if (operation instanceof DeleteOperation)
{
return new DeleteResponseProtocolOp(resultCode);
}
else if (operation instanceof ExtendedOperation)
{
return new ExtendedResponseProtocolOp(resultCode);
}
else if (operation instanceof ModifyDNOperation)
{
return new ModifyDNResponseProtocolOp(resultCode);
}
else if (operation instanceof ModifyOperation)
{
return new ModifyResponseProtocolOp(resultCode);
}
else if (operation instanceof SearchOperation)
{
return new SearchResultDoneProtocolOp(resultCode);
}
throw new RuntimeException("Not implemented for operation " + operation);
}
/** {@inheritDoc} */
@Override
public void sendSearchEntry(SearchOperation operation,
SearchResultEntry searchEntry) throws DirectoryException
{
OperationWithFutureResult op =
this.operationsInProgress.get(operation.getMessageID());
if (op != null)
{
((SearchResultHandler) op.futureResult.getResultHandler())
.handleEntry(from(searchEntry));
if (keepStats)
{
this.statTracker.updateMessageWritten(new LDAPMessage(operation
.getMessageID(), new SearchResultEntryProtocolOp(searchEntry)));
}
}
}
/** {@inheritDoc} */
@Override
public boolean sendSearchReference(SearchOperation operation,
SearchResultReference searchReference) throws DirectoryException
{
OperationWithFutureResult op =
this.operationsInProgress.get(operation.getMessageID());
if (op != null)
{
((SearchResultHandler) op.futureResult.getResultHandler())
.handleReference(from(searchReference));
if (keepStats)
{
this.statTracker.updateMessageWritten(new LDAPMessage(operation
.getMessageID(), new SearchResultReferenceProtocolOp(
searchReference)));
}
}
return connectionValid;
}
/** {@inheritDoc} */
@Override
protected boolean sendIntermediateResponseMessage(
IntermediateResponse intermediateResponse)
{
// if (keepStats)
// {
// this.statTracker.updateMessageWritten(new LDAPMessage(
// intermediateResponse.getOperation().getMessageID(),
// new IntermediateResponseProtocolOp(intermediateResponse.getOID())));
// }
throw new RuntimeException("Not implemented");
}
/** {@inheritDoc} */
@Override
public void setAuthUser(String authUser)
{
this.authUser = authUser;
}
/**
* {@inheritDoc}
*
* @param sendNotification
* not used with HTTP.
*/
@Override
public void disconnect(DisconnectReason disconnectReason,
boolean sendNotification, Message message)
{
// Set a flag indicating that the connection is being terminated so
// that no new requests will be accepted. Also cancel all operations
// in progress.
synchronized (opsInProgressLock)
{
// If we are already in the middle of a disconnect, then don't
// do anything.
if (disconnectRequested)
{
return;
}
disconnectRequested = true;
}
if (keepStats)
{
statTracker.updateDisconnect();
}
if (connectionID >= 0)
{
DirectoryServer.connectionClosed(this);
}
// Indicate that this connection is no longer valid.
connectionValid = false;
if (message != null)
{
MessageBuilder msgBuilder = new MessageBuilder();
msgBuilder.append(disconnectReason.getClosureMessage());
msgBuilder.append(": ");
msgBuilder.append(message);
cancelAllOperations(new CancelRequest(true, msgBuilder.toMessage()));
}
else
{
cancelAllOperations(new CancelRequest(true, disconnectReason
.getClosureMessage()));
}
finalizeConnectionInternal();
this.connectionHandler.removeClientConnection(this);
logDisconnect(this, disconnectReason, message);
}
/** {@inheritDoc} */
@Override
public String getMethod()
{
return this.method;
}
/** {@inheritDoc} */
@Override
public String getQuery()
{
return this.query;
}
/** {@inheritDoc} */
@Override
public int getStatusCode()
{
return this.statusCode.get();
}
/** {@inheritDoc} */
@Override
public String getUserAgent()
{
return this.userAgent;
}
/** {@inheritDoc} */
@Override
public Collection<Operation> getOperationsInProgress()
{
Collection<OperationWithFutureResult> values =
operationsInProgress.values();
Collection<Operation> results = new ArrayList<Operation>(values.size());
for (OperationWithFutureResult op : values)
{
results.add(op.operation);
}
return results;
}
/** {@inheritDoc} */
@Override
public Operation getOperationInProgress(int messageID)
{
OperationWithFutureResult op = operationsInProgress.get(messageID);
if (op != null)
{
return op.operation;
}
return null;
}
/**
* Adds the passed in operation to the in progress list along with the
* associated future.
*
* @param operation
* the operation to add to the in progress list
* @param futureResult
* the future associated to the operation.
* @throws DirectoryException
* If an error occurs
*/
void addOperationInProgress(Operation operation,
AsynchronousFutureResult<Result, ResultHandler<? super Result>>
futureResult) throws DirectoryException
{
synchronized (opsInProgressLock)
{
// If we're already in the process of disconnecting the client,
// then reject the operation.
if (disconnectRequested)
{
Message message = WARN_CLIENT_DISCONNECT_IN_PROGRESS.get();
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
}
operationsInProgress.put(operation.getMessageID(),
new OperationWithFutureResult(operation, futureResult));
}
}
/** {@inheritDoc} */
@Override
public boolean removeOperationInProgress(int messageID)
{
final OperationWithFutureResult previousValue =
operationsInProgress.remove(messageID);
if (previousValue != null)
{
operationsPerformed.incrementAndGet();
final Operation operation = previousValue.operation;
if (operation.getOperationType() == OperationType.ABANDON)
{
if (keepStats && operation.getResultCode() == ResultCode.CANCELED)
{
statTracker.updateAbandonedOperation();
}
}
}
return previousValue != null;
}
/** {@inheritDoc} */
@Override
public CancelResult cancelOperation(int messageID,
CancelRequest cancelRequest)
{
OperationWithFutureResult op = operationsInProgress.remove(messageID);
if (op != null)
{
op.futureResult.handleErrorResult(ErrorResultException
.newErrorResult(org.forgerock.opendj.ldap.ResultCode.CANCELLED));
return op.operation.cancel(cancelRequest);
}
return new CancelResult(ResultCode.NO_SUCH_OPERATION, null);
}
private int calcSSF(Object ssf)
{
if (ssf instanceof Number)
{
return ((Number) ssf).intValue();
}
else if (ssf instanceof String)
{
try
{
return Integer.parseInt((String) ssf);
}
catch (IllegalArgumentException ignored)
{
// We cannot do much about it. Just log it.
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, ignored);
}
}
}
return 0;
}
/** {@inheritDoc} */
@Override
public void cancelAllOperations(CancelRequest cancelRequest)
{
synchronized (opsInProgressLock)
{
try
{
for (OperationWithFutureResult op : operationsInProgress.values())
{
try
{
op.futureResult.handleErrorResult(ErrorResultException
.newErrorResult(org.forgerock.opendj.ldap.ResultCode.CANCELLED));
op.operation.abort(cancelRequest);
if (keepStats)
{
statTracker.updateAbandonedOperation();
}
}
catch (Exception e)
{ // make sure all operations are cancelled, no matter what
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
operationsInProgress.clear();
}
catch (Exception e)
{ // TODO JNR should I keep this catch?
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
}
/** {@inheritDoc} */
@Override
public void cancelAllOperationsExcept(CancelRequest cancelRequest,
int messageID)
{
synchronized (opsInProgressLock)
{
OperationWithFutureResult toKeep = operationsInProgress.remove(messageID);
try
{
cancelAllOperations(cancelRequest);
}
finally
{ // Ensure we always put back this operation
operationsInProgress.put(messageID, toKeep);
}
}
}
/** {@inheritDoc} */
@Override
public long getNumberOfOperations()
{
return this.operationsPerformed.get();
}
/** {@inheritDoc} */
@Override
public String getMonitorSummary()
{
StringBuilder buffer = new StringBuilder();
buffer.append("connID=\"");
buffer.append(getConnectionID());
buffer.append("\" connectTime=\"");
buffer.append(getConnectTimeString());
buffer.append("\" source=\"");
buffer.append(getClientAddress());
buffer.append(":");
buffer.append(getClientPort());
buffer.append("\" destination=\"");
buffer.append(getServerAddress());
buffer.append(":");
buffer.append(connectionHandler.getListenPort());
buffer.append("\" authDN=\"");
DN authDN = getAuthenticationInfo().getAuthenticationDN();
if (authDN != null)
{
authDN.toString(buffer);
}
return buffer.toString();
}
private InetAddress toInetAddress(String address)
{
try
{
return InetAddress.getByName(address);
}
catch (UnknownHostException e)
{
throw new RuntimeException("Should never happen", e);
}
}
/** {@inheritDoc} */
@Override
public void toString(StringBuilder buffer)
{
buffer.append("HTTP client connection from ");
buffer.append(getClientAddress()).append(":").append(getClientPort());
buffer.append(" to ");
buffer.append(getServerAddress()).append(":").append(getServerPort());
}
/**
* Returns the statTracker for this connection handler.
*
* @return the statTracker for this connection handler
*/
public HTTPStatistics getStatTracker()
{
return statTracker;
}
/** {@inheritDoc} */
@Override
public int getSSF()
{
return securityStrengthFactor;
}
/** {@inheritDoc} */
@Override
public boolean isConnectionValid()
{
return connectionValid;
}
/** {@inheritDoc} */
@Override
public boolean isInnerConnection()
{
return true;
}
/** {@inheritDoc} */
@Override
public void log(int statusCode)
{
if (this.statusCode.compareAndSet(0, statusCode))
{ // this request was not logged before
HTTPAccessLogger.logRequestInfo(this);
}
}
}