/* * 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 legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * 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 legal-notices/CDDLv1_0.txt. * 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-2015 ForgeRock AS */ package org.opends.server.protocols.http; import static org.forgerock.opendj.adapter.server3x.Converters.from; import static org.forgerock.opendj.adapter.server3x.Converters.getResponseResult; import static org.forgerock.opendj.ldap.LdapException.newLdapException; import static org.opends.messages.ProtocolMessages.WARN_CLIENT_DISCONNECT_IN_PROGRESS; import static org.opends.server.loggers.CommonAudit.DEFAULT_TRANSACTION_ID; import static org.opends.server.loggers.AccessLogger.logDisconnect; 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 org.forgerock.http.MutableUri; import org.forgerock.http.header.MalformedHeaderException; import org.forgerock.http.header.TransactionIdHeader; import org.forgerock.http.protocol.Request; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizableMessageBuilder; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.LdapException; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchResultHandler; import org.forgerock.opendj.ldap.responses.Result; import org.forgerock.opendj.ldap.spi.LdapPromiseImpl; import org.forgerock.services.context.AttributesContext; import org.forgerock.services.context.ClientContext; import org.forgerock.services.context.Context; 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.core.ServerContext; import org.opends.server.loggers.HTTPAccessLogger; import org.opends.server.loggers.HTTPRequestInfo; 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.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.SearchResultEntry; import org.opends.server.types.SearchResultReference; /** * 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 LdapPromiseImpl} to ensure they are both atomically added * and removed from the {@link HTTPClientConnection#operationsInProgress} Map. */ private static class OperationWithPromise { final Operation operation; final LdapPromiseImpl<Result> promise; public OperationWithPromise(Operation operation, LdapPromiseImpl<Result> promise) { this.operation = operation; this.promise = promise; } @Override public String toString() { return operation.toString(); } } /** Search Operation with a promise. */ private static final class SearchOperationWithPromise extends OperationWithPromise { final SearchResultHandler entryHandler; public SearchOperationWithPromise( Operation operation, LdapPromiseImpl<Result> promise, SearchResultHandler entryHandler) { super(operation, promise); this.entryHandler = entryHandler; } } /** The tracer object for the debug logger. */ private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** * 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 OperationWithPromise}) of all operations * currently in progress on this connection. */ private final Map<Integer, OperationWithPromise> operationsInProgress = new ConcurrentHashMap<>(); /** * 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; /** Total execution time for this request. */ private final AtomicLong totalProcessingTime = new AtomicLong(); /** The protocol in use for this client connection. */ private final 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 final AtomicInteger statusCode = new AtomicInteger(0); /** The client (remote) address. */ private final String clientAddress; /** The client (remote) host name. */ private String clientHost; /** The client (remote) port. */ private final int clientPort; /** The remote (client) address. */ private final InetAddress remoteAddress; /** The server (local) address. */ private final String serverAddress; /** The server (local) host name. */ private String serverHost; /** The server (local) port. */ private final int serverPort; /** The local (server) address. */ private final InetAddress localAddress; /** Whether this connection is secure. */ private boolean isSecure; /** Security-Strength Factor extracted from the request attribute. */ private final int securityStrengthFactor; /** TransactionId for tracking of ForgeRock stack transactions. */ private final String transactionId; /** * Constructs an instance of this class. * @param serverContext * The server context. * @param connectionHandler * the connection handler that accepted this connection * @param context * represents the context of this client connection. */ public HTTPClientConnection(ServerContext serverContext, HTTPConnectionHandler connectionHandler, Context context, Request request) { this.connectionHandler = connectionHandler; final ClientContext clientCtx = context.asContext(ClientContext.class); // Memorize all the fields we need from the request before Grizzly decides to recycle it this.clientAddress = clientCtx.getRemoteAddress(); this.remoteAddress = toInetAddress(clientAddress); this.clientPort = clientCtx.getRemotePort(); this.isSecure = clientCtx.isSecure(); final MutableUri uri = request.getUri(); this.serverAddress = uri.getHost(); this.localAddress = toInetAddress(serverAddress); this.serverPort = uri.getPort(); this.securityStrengthFactor = calcSSF( context.asContext(AttributesContext.class).getAttributes().get(SERVLET_SSF_CONSTANT)); this.method = request.getMethod(); this.query = uri.getQuery(); this.protocol = request.getVersion(); this.userAgent = clientCtx.getUserAgent(); this.statTracker = this.connectionHandler.getStatTracker(); this.keepStats = connectionHandler.keepStats(); if (this.keepStats) { this.statTracker.updateConnect(); this.useNanoTime = DirectoryServer.getUseNanoTime(); } this.transactionId = getTransactionId(serverContext, request); this.connectionID = DirectoryServer.newConnectionAccepted(this); } private String getTransactionId(ServerContext serverContext, Request request) { if (serverContext.getCommonAudit().shouldTrustTransactionIds()) { try { TransactionIdHeader txHeader = request.getHeaders().get(TransactionIdHeader.class); return txHeader == null ? DEFAULT_TRANSACTION_ID : txHeader.getTransactionId().getValue(); } catch (MalformedHeaderException e) { // ignore it } } return DEFAULT_TRANSACTION_ID; } @Override public String getAuthUser() { return this.authUser; } @Override public long getConnectionID() { return connectionID; } @Override public HTTPConnectionHandler getConnectionHandler() { return connectionHandler; } @Override public long getTotalProcessingTime() { return totalProcessingTime.get(); } @Override public String getProtocol() { return protocol; } @Override public String getClientAddress() { return clientAddress; } @Override public String getClientHost() { return clientHost; } @Override public int getClientPort() { return clientPort; } @Override public String getServerAddress() { return serverAddress; } @Override public String getServerHost() { return serverHost; } @Override public int getServerPort() { return serverPort; } @Override public InetAddress getRemoteAddress() { return remoteAddress; } @Override public InetAddress getLocalAddress() { return localAddress; } @Override public String getTransactionId() { return transactionId; } @Override public boolean isSecure() { return isSecure; } @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); } OperationWithPromise op = this.operationsInProgress.get(operation.getMessageID()); if (op != null) { try { op.promise.handleResult(getResponseResult(operation)); if (keepStats) { this.statTracker.updateMessageWritten( new LDAPMessage(operation.getMessageID(), toResponseProtocolOp(operation))); } } catch (LdapException e) { op.promise.handleException(e); } } } private long getProcessingTime(Operation operation) { if (useNanoTime) { return operation.getProcessingNanoTime(); } return operation.getProcessingTime(); } private ProtocolOp toResponseProtocolOp(Operation operation) { final int resultCode = operation.getResultCode().intValue(); 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); } @Override public void sendSearchEntry(SearchOperation operation, SearchResultEntry searchEntry) throws DirectoryException { SearchOperationWithPromise op = (SearchOperationWithPromise) this.operationsInProgress.get(operation.getMessageID()); if (op != null) { op.entryHandler.handleEntry(from(searchEntry)); if (keepStats) { this.statTracker.updateMessageWritten( new LDAPMessage(operation.getMessageID(), new SearchResultEntryProtocolOp(searchEntry))); } } } @Override public boolean sendSearchReference(SearchOperation operation, SearchResultReference searchReference) throws DirectoryException { SearchOperationWithPromise op = (SearchOperationWithPromise) this.operationsInProgress.get(operation.getMessageID()); if (op != null) { op.entryHandler.handleReference(from(searchReference)); if (keepStats) { this.statTracker.updateMessageWritten( new LDAPMessage(operation.getMessageID(), new SearchResultReferenceProtocolOp(searchReference))); } } return connectionValid; } @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"); } @Override public void setAuthUser(String authUser) { this.authUser = authUser; } /** * {@inheritDoc} * * @param sendNotification * not used with HTTP. */ @Override public void disconnect(DisconnectReason disconnectReason, boolean sendNotification, LocalizableMessage 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) { LocalizableMessageBuilder msgBuilder = new LocalizableMessageBuilder(); 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); } @Override public String getMethod() { return this.method; } @Override public String getQuery() { return this.query; } @Override public int getStatusCode() { return this.statusCode.get(); } @Override public String getUserAgent() { return this.userAgent; } @Override public Collection<Operation> getOperationsInProgress() { Collection<OperationWithPromise> values = operationsInProgress.values(); Collection<Operation> results = new ArrayList<>(values.size()); for (OperationWithPromise op : values) { results.add(op.operation); } return results; } @Override public Operation getOperationInProgress(int messageID) { OperationWithPromise op = operationsInProgress.get(messageID); if (op != null) { return op.operation; } return null; } /** * Adds the passed in search operation to the in progress list along with the * associated promise and the {@code SearchResultHandler}. * * @param operation * the operation to add to the in progress list * @param promise * the promise associated to the operation * @param searchResultHandler * the search result handler associated to the promise result * @throws DirectoryException * If an error occurs */ void addOperationInProgress(Operation operation, LdapPromiseImpl<Result> promise, SearchResultHandler searchResultHandler) throws DirectoryException { if (searchResultHandler != null) { addOperationWithPromise(new SearchOperationWithPromise(operation, promise, searchResultHandler)); } else { addOperationWithPromise(new OperationWithPromise(operation, promise)); } } private void addOperationWithPromise(OperationWithPromise opPromise) throws DirectoryException { synchronized (opsInProgressLock) { // If we're already in the process of disconnecting the client, then reject the operation. if (disconnectRequested) { LocalizableMessage message = WARN_CLIENT_DISCONNECT_IN_PROGRESS.get(); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } operationsInProgress.put(opPromise.operation.getMessageID(), opPromise); } } @Override public boolean removeOperationInProgress(int messageID) { final OperationWithPromise previousValue = operationsInProgress.remove(messageID); if (previousValue != null) { operationsPerformed.incrementAndGet(); final Operation operation = previousValue.operation; if (operation.getOperationType() == OperationType.ABANDON && keepStats && operation.getResultCode() == ResultCode.CANCELLED) { statTracker.updateAbandonedOperation(); } } return previousValue != null; } @Override public CancelResult cancelOperation(int messageID, CancelRequest cancelRequest) { OperationWithPromise op = operationsInProgress.remove(messageID); if (op != null) { op.promise.handleException(newLdapException(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. logger.traceException(ignored); } } return 0; } @Override public void cancelAllOperations(CancelRequest cancelRequest) { synchronized (opsInProgressLock) { try { for (OperationWithPromise op : operationsInProgress.values()) { try { op.promise.handleException(newLdapException(ResultCode.CANCELLED)); op.operation.abort(cancelRequest); if (keepStats) { statTracker.updateAbandonedOperation(); } } catch (Exception e) { // Make sure all operations are cancelled, no matter what logger.traceException(e); } } operationsInProgress.clear(); } catch (Exception e) { // TODO JNR should I keep this catch? logger.traceException(e); } } } @Override public void cancelAllOperationsExcept(CancelRequest cancelRequest, int messageID) { synchronized (opsInProgressLock) { OperationWithPromise toKeep = operationsInProgress.remove(messageID); try { cancelAllOperations(cancelRequest); } finally { // Ensure we always put back this operation operationsInProgress.put(messageID, toKeep); } } } @Override public long getNumberOfOperations() { return this.operationsPerformed.get(); } @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); } } @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; } @Override public int getSSF() { return securityStrengthFactor; } @Override public boolean isConnectionValid() { return connectionValid; } @Override public boolean isInnerConnection() { return true; } @Override public void log(int statusCode) { if (this.statusCode.compareAndSet(0, statusCode)) { // This request was not logged before HTTPAccessLogger.logRequestInfo(this); } } }