| opends/build.xml | ●●●●● patch | view | raw | blame | history | |
| opends/ivy.xml | ●●●●● patch | view | raw | blame | history | |
| opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java | ●●●●● patch | view | raw | blame | history | |
| opends/src/server/org/opends/server/protocols/http/HTTPAuthenticationConfig.java | ●●●●● patch | view | raw | blame | history | |
| opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java | ●●●●● patch | view | raw | blame | history | |
| opends/src/server/org/opends/server/protocols/http/HTTPConnectionHandler.java | ●●●●● patch | view | raw | blame | history | |
| opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java | ●●●●● patch | view | raw | blame | history | |
| opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java | ●●●●● patch | view | raw | blame | history |
opends/build.xml
@@ -181,6 +181,17 @@ <fileset id="opendj.runtime.jars" dir="${lib.dir}"> <include name="**/jar/*.jar" /> <include name="**/bundle/*.jar" /> <exclude name="**/assertj-core.jar" /> <exclude name="**/mockito-core.jar" /> <exclude name="**/objenesis.jar" /> </fileset> <fileset id="opendj.test.jars" dir="${lib.dir}"> <include name="**/assertj-core.jar" /> <include name="**/mockito-core.jar" /> <include name="**/hamcrest-core.jar" /> <include name="**/objenesis.jar" /> </fileset> <!-- Prevent ant runtime from being included on classpath during @@ -1590,6 +1601,7 @@ destdir="${classes.dir}"> <classpath> <fileset refid="opendj.runtime.jars"/> <fileset refid="opendj.test.jars"/> <fileset dir="${build.dir}/build-tools"> <include name="build-tools.jar" /> </fileset> @@ -1626,6 +1638,7 @@ <javac srcdir="${unittest.testng.src.dir}" destdir="${unittest.classes.dir}" excludes="org/opends/server/snmp/**"> <classpath> <fileset refid="opendj.runtime.jars"/> <fileset refid="opendj.test.jars"/> <fileset dir="${testng.lib.dir}"> <include name="*.jar" /> @@ -2108,6 +2121,7 @@ <path refid="emma.lib" /> <fileset refid="opendj.runtime.jars"/> <fileset refid="opendj.test.jars"/> <!-- Needed by quicksetup tests --> <fileset dir="${build.dir}/build-tools"> @@ -2555,6 +2569,7 @@ includes="org/opends/server/snmp/**"> <classpath> <fileset refid="opendj.runtime.jars"/> <fileset refid="opendj.test.jars"/> <fileset dir="${testng.lib.dir}"> <include name="*.jar" /> opends/ivy.xml
@@ -27,7 +27,7 @@ ! --> <!-- Using entities prevent constantly declaring the same versions --> <!DOCTYPE ivy-module [ <!ENTITY grizzly.version "2.3-rc6"> <!ENTITY grizzly.version "2.3"> <!ENTITY opendj.sdk.version "3.0.0-SNAPSHOT"> <!ENTITY crest.version "2.0.0-SNAPSHOT"> ]> @@ -36,6 +36,8 @@ xsi:noNamespaceSchemaLocation="http://incubator.apache.org/ivy/schemas/ivy.xsd"> <info organisation="org.forgerock" module="opendj"/> <dependencies> <!-- compile + runtime libs --> <dependency org="javax.mail" name="mail" rev="1.4.5" /> <!-- subsequent versions are not compatible with java 6, so force to use this version only --> <dependency org="javax.servlet" name="javax.servlet-api" rev="[3.1-b02]" /> @@ -49,6 +51,11 @@ <exclude module="javax.servlet-api" /> </dependency> <!-- Test libs --> <!--dependency org="org.testng" name="testng" rev="6.8.1" /--> <dependency org="org.assertj" name="assertj-core" rev="1.0.0" /> <dependency org="org.mockito" name="mockito-core" rev="1.9.5" /> <!-- Force download of the source jars --> <!-- <dependency org="org.codehaus.jackson" name="jackson-core-asl" rev="1.9.2" conf="default->master,sources"/> opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
@@ -26,6 +26,7 @@ */ 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.ErrorLogger.*; import static org.opends.server.loggers.debug.DebugLogger.*; @@ -33,47 +34,76 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.text.ParseException; import java.util.Collection; import java.util.Map; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.forgerock.opendj.ldap.Connection; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.ErrorResultException; import org.forgerock.opendj.ldap.Filter; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.requests.BindRequest; import org.forgerock.opendj.ldap.requests.Requests; import org.forgerock.opendj.ldap.requests.SearchRequest; import org.forgerock.opendj.ldap.responses.BindResult; import org.forgerock.opendj.ldap.responses.SearchResultEntry; import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory; import org.opends.messages.Message; import org.opends.server.admin.std.server.ConnectionHandlerCfg; import org.opends.server.api.ClientConnection; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.schema.SchemaConstants; import org.opends.server.types.AddressMask; import org.opends.server.types.AuthenticationInfo; import org.opends.server.types.ByteString; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DisconnectReason; import org.opends.server.util.Base64; /** * Servlet {@link Filter} that collects information about client connections. */ final class CollectClientConnectionsFilter implements Filter final class CollectClientConnectionsFilter implements javax.servlet.Filter { /** HTTP Header sent by the client with HTTP basic authentication. */ static final String HTTP_BASIC_AUTH_HEADER = "Authorization"; /** The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** The connection handler that created this servlet filter. */ private final HTTPConnectionHandler connectionHandler; /** * Configures how to perform the search for the username prior to * authentication. */ private final HTTPAuthenticationConfig authConfig; /** * Constructs a new instance of this class. * * @param connectionHandler * the connection handler that accepted this connection * @param authenticationConfig * configures how to perform the search for the username prior to * authentication */ public CollectClientConnectionsFilter(HTTPConnectionHandler connectionHandler) public CollectClientConnectionsFilter( HTTPConnectionHandler connectionHandler, HTTPAuthenticationConfig authenticationConfig) { this.connectionHandler = connectionHandler; this.authConfig = authenticationConfig; } /** {@inheritDoc} */ @@ -100,19 +130,36 @@ return; } // TODO JNR handle authentication Connection connectionAdapter = new SdkConnectionAdapter(clientConnection); Connection connection = new SdkConnectionAdapter(clientConnection); // WARNING: This action triggers 3-4 others: // Set the connection for use with this request on the HttpServletRequest. // It will make Rest2LDAPContextFactory create an // AuthenticatedConnectionContext which will in turn ensure Rest2LDAP uses // the supplied Connection object request.setAttribute(Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION, connectionAdapter); String[] userPassword = extractUsernamePassword(request); if (userPassword != null && userPassword.length == 2) { AuthenticationInfo authInfo = authenticate(userPassword[0], userPassword[1], connection); if (authInfo != null) { clientConnection.setAuthenticationInfo(authInfo); // send the request further down the filter chain or pass to servlet chain.doFilter(request, response); /* * WARNING: This action triggers 3-4 others: Set the connection for * use with this request on the HttpServletRequest. It will make * Rest2LDAPContextFactory create an AuthenticatedConnectionContext * which will in turn ensure Rest2LDAP uses the supplied Connection * object */ request.setAttribute( Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION, connection); // send the request further down the filter chain or pass to servlet chain.doFilter(request, response); return; } } // The user could not be authenticated. Send an HTTP Basic authentication // challenge if HTTP Basic authentication is enabled. sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response); } catch (Exception e) { @@ -177,6 +224,185 @@ return true; } /** * Extracts the username and password from the request using one of the * enabled authentication mechanism: HTTP Basic authentication or HTTP Custom * headers. If no username and password can be obtained, then send back an * HTTP basic authentication challenge if HTTP basic authentication is * enabled. * * @param request * the request where to extract the username and password from * @return the array containing the username/password couple if both exist, * null otherwise */ String[] extractUsernamePassword(ServletRequest request) { HttpServletRequest req = (HttpServletRequest) request; // TODO Use session to reduce hits with search + bind? // Use proxied authorization control for session. if (authConfig.isCustomHeadersAuthenticationSupported()) { final String userName = req.getHeader(authConfig.getCustomHeaderUsername()); final String password = req.getHeader(authConfig.getCustomHeaderPassword()); if (userName != null && password != null) { return new String[] { userName, password }; } } if (authConfig.isBasicAuthenticationSupported()) { String httpBasicAuthHeader = req.getHeader(HTTP_BASIC_AUTH_HEADER); if (httpBasicAuthHeader != null) { String[] userPassword = parseUsernamePassword(httpBasicAuthHeader); if (userPassword != null) { return userPassword; } } } return null; } /** * Sends an Unauthorized status code and a challenge for HTTP Basic * authentication if HTTP basic authentication is enabled. * * @param response * where to send the Unauthorized status code. */ void sendUnauthorizedResponseWithHTTPBasicAuthChallenge( ServletResponse response) { HttpServletResponse resp = (HttpServletResponse) response; resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); if (authConfig.isBasicAuthenticationSupported()) { resp.setHeader("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\""); } } /** * Parses username and password from the authentication header used in HTTP * basic authentication. * * @param authHeader * the authentication header obtained from the request * @return an array containing the username at index 0 and the password at * index 1, or null if the header cannot be parsed successfully */ String[] parseUsernamePassword(String authHeader) { if (authHeader != null && (authHeader.startsWith("Basic") || authHeader.startsWith("basic"))) { // We received authentication info // Example received header: // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" String base64UserPassword = authHeader.substring("basic".length() + 1); try { // Example usage of base64: // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==" String userPassword = new String(Base64.decode(base64UserPassword)); String[] split = userPassword.split(":"); if (split.length == 2) { return split; } } catch (ParseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } } return null; } /** * Authenticates the user by doing a search on the user name + bind the * returned user entry DN, then return the authentication info if search and * bind were successful. * * @param userName * the user name to authenticate * @param password * the password to use with the user * @param connection * the connection to use for search and bind * @return the {@link AuthenticationInfo} for the supplied credentials, null * if authentication was unsuccessful. */ private AuthenticationInfo authenticate(String userName, String password, Connection connection) { // TODO JNR do the next steps in an async way SearchResultEntry resultEntry = searchUniqueEntryDN(userName, connection); if (resultEntry != null) { DN bindDN = resultEntry.getName(); if (bindDN != null && bind(bindDN.toString(), password, connection)) { return new AuthenticationInfo(to(resultEntry), to(bindDN), ByteString .valueOf(password), false); } } return null; } private SearchResultEntry searchUniqueEntryDN(String userName, Connection connection) { // use configured rights to find the user DN final Filter filter = Filter.format(authConfig.getSearchFilterTemplate(), userName); final SearchRequest searchRequest = Requests.newSearchRequest(authConfig.getSearchBaseDN(), authConfig .getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES); try { return connection.searchSingleEntry(searchRequest); } catch (ErrorResultException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } return null; } private boolean bind(String bindDN, String password, Connection connection) { BindRequest bindRequest = Requests.newSimpleBindRequest(bindDN, password.getBytes()); try { BindResult bindResult = connection.bind(bindRequest); return ResultCode.SUCCESS.equals(bindResult.getResultCode()); } catch (ErrorResultException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } return false; } /** {@inheritDoc} */ @Override public void destroy() opends/src/server/org/opends/server/protocols/http/HTTPAuthenticationConfig.java
New file @@ -0,0 +1,233 @@ /* * 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 org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.SearchScope; /** * Class holding the configuration for HTTP authentication. This is extracted * from the JSON config file or the config held in LDAP. */ class HTTPAuthenticationConfig { private boolean basicAuthenticationSupported; private boolean customHeadersAuthenticationSupported; private String customHeaderUsername; private String customHeaderPassword; private DN searchBaseDN; private SearchScope searchScope; private String searchFilterTemplate; /** * Returns whether HTTP basic authentication is supported. * * @return true if supported, false otherwise */ public boolean isBasicAuthenticationSupported() { return basicAuthenticationSupported; } /** * Sets whether HTTP basic authentication is supported. * * @param supported * the supported value */ public void setBasicAuthenticationSupported(boolean supported) { this.basicAuthenticationSupported = supported; } /** * Returns whether HTTP authentication via custom headers is supported. * * @return true if supported, false otherwise */ public boolean isCustomHeadersAuthenticationSupported() { return customHeadersAuthenticationSupported; } /** * Sets whether HTTP authentication via custom headers is supported. * * @param supported * the supported value */ public void setCustomHeadersAuthenticationSupported(boolean supported) { this.customHeadersAuthenticationSupported = supported; } /** * Returns the expected HTTP header for the username. This setting is only * used when HTTP authentication via custom headers is supported. * * @return the HTTP header for the username */ public String getCustomHeaderUsername() { return customHeaderUsername; } /** * Sets the expected HTTP header for the username. This setting only takes * effect when HTTP authentication via custom headers is supported. * * @param customHeaderUsername * the HTTP header for the username */ public void setCustomHeaderUsername(String customHeaderUsername) { this.customHeaderUsername = customHeaderUsername; } /** * Returns the expected HTTP header for the password. This setting is only * used when HTTP authentication via custom headers is supported. * * @return the HTTP header for the password */ public String getCustomHeaderPassword() { return customHeaderPassword; } /** * Sets the expected HTTP header for the password. This setting only takes * effect when HTTP authentication via custom headers is supported. * * @param customHeaderPassword * the HTTP header for the password */ public void setCustomHeaderPassword(String customHeaderPassword) { this.customHeaderPassword = customHeaderPassword; } /** * Returns the base DN to use when searching the entry corresponding to the * authenticating user. * * @return the base DN to use when searching the authenticating user */ public DN getSearchBaseDN() { return searchBaseDN; } /** * Sets the base DN to use when searching the entry corresponding to the * authenticating user. * * @param searchBaseDN * the base DN to use when searching the authenticating user */ public void setSearchBaseDN(DN searchBaseDN) { this.searchBaseDN = searchBaseDN; } /** * Returns the search scope to use when searching the entry corresponding to * the authenticating user. * * @return the search scope to use when searching the authenticating user */ public SearchScope getSearchScope() { return searchScope; } /** * Sets the search scope to use when searching the entry corresponding to the * authenticating user. * * @param searchScope * the search scope to use when searching the authenticating user */ public void setSearchScope(SearchScope searchScope) { this.searchScope = searchScope; } /** * Returns the search filter template to use when searching the entry * corresponding to the authenticating user. * * @return the search filter template to use when searching the authenticating * user */ public String getSearchFilterTemplate() { return searchFilterTemplate; } /** * Sets the search filter template to use when searching the entry * corresponding to the authenticating user. * * @param searchFilterTemplate * the search filter template to use when searching the * authenticating user */ public void setSearchFilterTemplate(String searchFilterTemplate) { this.searchFilterTemplate = searchFilterTemplate; } /** {@inheritDoc} */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("basicAuth: "); if (!basicAuthenticationSupported) { sb.append("not "); } sb.append("supported, "); sb.append("customHeadersAuth: "); if (customHeadersAuthenticationSupported) { sb.append("usernameHeader=\"").append(customHeaderUsername).append("\","); sb.append("passwordHeader=\"").append(customHeaderPassword).append("\""); } else { sb.append("not supported, "); } sb.append("searchBaseDN: \"").append(searchBaseDN).append("\""); sb.append("searchScope: \"").append(searchScope).append("\""); sb.append("searchFilterTemplate: \"").append(searchFilterTemplate).append( "\""); return sb.toString(); } } opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
@@ -41,6 +41,7 @@ import javax.servlet.ServletRequest; 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; @@ -86,15 +87,22 @@ { final Operation operation; final AsynchronousFutureResult<Result, SearchResultHandler> futureResult; final AsynchronousFutureResult<Result, ResultHandler<? super Result>> futureResult; public OperationWithFutureResult(Operation operation, AsynchronousFutureResult<Result, SearchResultHandler> futureResult) 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. */ @@ -282,7 +290,8 @@ this.operationsInProgress.get(operation.getMessageID()); if (op != null) { op.futureResult.getResultHandler().handleEntry(from(searchEntry)); ((SearchResultHandler) op.futureResult.getResultHandler()) .handleEntry(from(searchEntry)); } } @@ -295,7 +304,8 @@ this.operationsInProgress.get(operation.getMessageID()); if (op != null) { op.futureResult.getResultHandler().handleReference(from(searchReference)); ((SearchResultHandler) op.futureResult.getResultHandler()) .handleReference(from(searchReference)); } return connectionValid; } @@ -402,8 +412,8 @@ * If an error occurs */ void addOperationInProgress(Operation operation, AsynchronousFutureResult<Result, SearchResultHandler> futureResult) throws DirectoryException AsynchronousFutureResult<Result, ResultHandler<? super Result>> futureResult) throws DirectoryException { synchronized (opsInProgressLock) { opends/src/server/org/opends/server/protocols/http/HTTPConnectionHandler.java
@@ -52,10 +52,12 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterRegistration; import javax.servlet.ServletException; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; import org.forgerock.json.fluent.JsonValue; import org.forgerock.json.resource.CollectionResourceProvider; @@ -63,6 +65,7 @@ import org.forgerock.json.resource.Resources; import org.forgerock.json.resource.Router; import org.forgerock.json.resource.servlet.HttpServlet; import org.forgerock.opendj.ldap.SearchScope; import org.forgerock.opendj.rest2ldap.AuthorizationPolicy; import org.forgerock.opendj.rest2ldap.Rest2LDAP; import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory; @@ -70,7 +73,6 @@ import org.glassfish.grizzly.http.server.NetworkListener; import org.glassfish.grizzly.http.server.ServerConfiguration; import org.glassfish.grizzly.nio.transport.TCPNIOTransport; import org.glassfish.grizzly.servlet.FilterRegistration; import org.glassfish.grizzly.servlet.ServletRegistration; import org.glassfish.grizzly.servlet.WebappContext; import org.glassfish.grizzly.ssl.SSLEngineConfigurator; @@ -714,18 +716,24 @@ final String urlPattern = "/*"; final WebappContext ctx = new WebappContext(servletName); Filter filter = new CollectClientConnectionsFilter(this); final JsonValue configuration = parseJsonConfiguration(getFileForPath(this.currentConfig .getConfigFile())); final HTTPAuthenticationConfig authenticationConfig = getAuthenticationConfig(configuration); javax.servlet.Filter filter = new CollectClientConnectionsFilter(this, authenticationConfig); FilterRegistration filterReg = ctx.addFilter("collectClientConnections", filter); // TODO JNR this is not working // filterReg.addMappingForServletNames(EnumSet.allOf( // DispatcherType.class), servletName); filterReg.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), urlPattern); true, urlPattern); ConnectionFactory connFactory = getConnectionFactory(getFileForPath(this.currentConfig .getConfigFile())); ConnectionFactory connFactory = getConnectionFactory(configuration); final ServletRegistration reg = ctx.addServlet(servletName, new HttpServlet(connFactory, @@ -757,19 +765,38 @@ } } private ConnectionFactory getConnectionFactory(File configFile) throws Exception private HTTPAuthenticationConfig getAuthenticationConfig( final JsonValue configuration) { // Parse the config file. final Object content = JSON_MAPPER.readValue(configFile, Object.class); if (!(content instanceof Map)) { throw new ServletException("Servlet configuration file '" + configFile + "' does not contain a valid JSON configuration"); } final JsonValue configuration = new JsonValue(content); final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig(); final JsonValue val = configuration.get("authenticationFilter"); result.setBasicAuthenticationSupported(asBool(val, "supportHTTPBasicAuthentication")); result.setCustomHeadersAuthenticationSupported(asBool(val, "supportAltAuthentication")); result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader") .asString()); result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader") .asString()); final String searchBaseDN = asString(val, "searchBaseDN"); result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN)); result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope"))); result.setSearchFilterTemplate(asString(val, "searchFilterTemplate")); return result; } // Create the router. private String asString(JsonValue value, String key) { return value.get(key).required().asString(); } private boolean asBool(JsonValue value, String key) { return value.get(key).defaultTo(false).asBoolean(); } private ConnectionFactory getConnectionFactory(final JsonValue configuration) { final Router router = new Router(); final JsonValue mappings = configuration.get("servlet").get("mappings").required(); @@ -784,6 +811,23 @@ return Resources.newInternalConnectionFactory(router); } private JsonValue parseJsonConfiguration(File configFile) throws IOException, JsonParseException, JsonMappingException, ServletException { // Parse the config file. final Object content = JSON_MAPPER.readValue(configFile, Object.class); if (!(content instanceof Map)) { throw new ServletException("Servlet configuration file '" + configFile + "' does not contain a valid JSON configuration"); } // TODO JNR should we restrict the possible configurations in this file? // Should we remove any config that does not make any sense to the // HTTP Connection Handler? return new JsonValue(content); } private void stopHttpServer() { TRACER.debugInfo("Stopping HTTP server..."); opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java
@@ -31,6 +31,7 @@ import static org.opends.server.loggers.debug.DebugLogger.*; import java.util.LinkedHashSet; import java.util.concurrent.atomic.AtomicInteger; import org.forgerock.opendj.ldap.AbstractAsynchronousConnection; import org.forgerock.opendj.ldap.ConnectionEventListener; @@ -49,16 +50,20 @@ import org.forgerock.opendj.ldap.requests.ModifyDNRequest; import org.forgerock.opendj.ldap.requests.ModifyRequest; import org.forgerock.opendj.ldap.requests.SearchRequest; import org.forgerock.opendj.ldap.requests.SimpleBindRequest; import org.forgerock.opendj.ldap.requests.UnbindRequest; import org.forgerock.opendj.ldap.responses.BindResult; import org.forgerock.opendj.ldap.responses.CompareResult; import org.forgerock.opendj.ldap.responses.ExtendedResult; import org.forgerock.opendj.ldap.responses.Result; import org.opends.server.core.BindOperationBasis; import org.opends.server.core.QueueingStrategy; import org.opends.server.core.SearchOperationBasis; import org.opends.server.core.WorkQueueStrategy; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.types.ByteString; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.Operation; import com.forgerock.opendj.util.AsynchronousFutureResult; @@ -76,11 +81,11 @@ /** The HTTP client connection being "adapted". */ private final HTTPClientConnection clientConnection; /** FIXME: do not use constants. */ private int messageID; /** FIXME: do not use constants. */ private long operationID; /** * The next message ID (and operation ID) that should be used for this * connection. */ private AtomicInteger nextMessageID = new AtomicInteger(0); /** The queueing strategy used for this connection. */ private QueueingStrategy queueingStrategy = new WorkQueueStrategy(); @@ -102,6 +107,34 @@ this.clientConnection = clientConnection; } private <R extends Result> FutureResult<R> enqueueOperation( Operation operation, ResultHandler<? super R> resultHandler) { // TODO JNR set requestID, but where to get it? final AsynchronousFutureResult<R, ResultHandler<? super R>> futureResult = new AsynchronousFutureResult<R, ResultHandler<? super R>>(resultHandler); try { clientConnection.addOperationInProgress(operation, (AsynchronousFutureResult) futureResult); queueingStrategy.enqueueRequest(operation); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } clientConnection.removeOperationInProgress(operation.getMessageID()); // TODO JNR add error message?? futureResult.handleErrorResult(ErrorResultException.newErrorResult( ResultCode.OPERATIONS_ERROR, e)); } return futureResult; } /** {@inheritDoc} */ @Override public FutureResult<Void> abandonAsync(AbandonRequest request) @@ -146,12 +179,15 @@ IntermediateResponseHandler intermediateResponseHandler, ResultHandler<? super BindResult> resultHandler) { // BindOperationBasis operation = // new BindOperationBasis(clientConnection, operationID, messageID, // to(request.getControls()), "3", to(request.getName()), "", // getCredentials(new byte[] {})); // TODO Auto-generated method stub throw new RuntimeException("Not implemented"); int messageID = nextMessageID.get(); String userName = request.getName(); byte[] password = ((SimpleBindRequest) request).getPassword(); BindOperationBasis operation = new BindOperationBasis(clientConnection, messageID, messageID, to(request.getControls()), "3", to(userName), ByteString .wrap(password)); return enqueueOperation(operation, resultHandler); } /** {@inheritDoc} */ @@ -243,35 +279,15 @@ { // TODO JNR attributes LinkedHashSet<String> attributes = null; SearchOperationBasis op2 = new SearchOperationBasis(clientConnection, operationID, messageID, final int messageID = nextMessageID.getAndIncrement(); SearchOperationBasis operation = new SearchOperationBasis(clientConnection, messageID, messageID, to(request.getControls()), to(valueOf(request.getName())), to(request.getScope()), to(request.getDereferenceAliasesPolicy()), request.getSizeLimit(), request.getTimeLimit(), request .isTypesOnly(), to(request.getFilter()), attributes); // TODO JNR set requestID final AsynchronousFutureResult<Result, SearchResultHandler> futureResult = new AsynchronousFutureResult<Result, SearchResultHandler>(resultHandler); try { clientConnection.addOperationInProgress(op2, futureResult); queueingStrategy.enqueueRequest(op2); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } clientConnection.removeOperationInProgress(messageID); // TODO JNR add error message?? futureResult.handleErrorResult(ErrorResultException.newErrorResult( ResultCode.OPERATIONS_ERROR, e)); } return futureResult; return enqueueOperation(operation, resultHandler); } /** {@inheritDoc} */ opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
New file @@ -0,0 +1,137 @@ /* * 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.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.opends.server.DirectoryServerTestCase; import org.opends.server.util.Base64; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class CollectClientConnectionsFilterTest extends DirectoryServerTestCase { private static final String AUTHORIZATION = CollectClientConnectionsFilter.HTTP_BASIC_AUTH_HEADER; private static final String USERNAME = "Aladdin"; private static final String PASSWORD = "open sesame"; private static final String BASE64_USERPASS = Base64 .encode((USERNAME + ":" + PASSWORD).getBytes()); private HTTPAuthenticationConfig authConfig = new HTTPAuthenticationConfig(); private CollectClientConnectionsFilter filter = new CollectClientConnectionsFilter(null, authConfig); @DataProvider(name = "Invalid HTTP basic auth strings") public Object[][] getInvalidHttpBasicAuthStrings() { return new Object[][] { { null }, { "bla" }, { "basic " + Base64.encode("la:bli:blu".getBytes()) } }; } @Test(dataProvider = "Invalid HTTP basic auth strings") public void parseUsernamePasswordFromInvalidAuthZHeader(String authZHeader) { assertThat(filter.parseUsernamePassword(authZHeader)).isNull(); } @DataProvider(name = "Valid HTTP basic auth strings") public Object[][] getValidHttpBasicAuthStrings() { return new Object[][] { { "basic " + BASE64_USERPASS }, { "Basic " + BASE64_USERPASS } }; } @Test(dataProvider = "Valid HTTP basic auth strings") public void parseUsernamePasswordFromValidAuthZHeader(String authZHeader) { assertThat(filter.parseUsernamePassword(authZHeader)).containsExactly( USERNAME, PASSWORD); } @Test public void sendUnauthorizedResponseWithHttpBasicAuthWillChallengeUserAgent() { authConfig.setBasicAuthenticationSupported(true); HttpServletResponse response = mock(HttpServletResponse.class); filter.sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response); verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); verify(response).setHeader("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\""); } @Test public void sendUnauthorizedResponseWithoutHttpBasicAuthWillNotChallengeUserAgent() { authConfig.setBasicAuthenticationSupported(true); HttpServletResponse response = mock(HttpServletResponse.class); filter.sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response); verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); } @Test public void extractUsernamePasswordHttpBasicAuthWillAcceptUserAgent() { authConfig.setBasicAuthenticationSupported(true); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getHeader(AUTHORIZATION)).thenReturn( "Basic " + BASE64_USERPASS); assertThat(filter.extractUsernamePassword(request)).containsExactly( USERNAME, PASSWORD); } @Test public void extractUsernamePasswordCustomHeaders() { final String customHeaderUsername = "X-OpenIDM-Username"; final String customHeaderPassword = "X-OpenIDM-Password"; authConfig.setCustomHeadersAuthenticationSupported(true); authConfig.setCustomHeaderUsername(customHeaderUsername); authConfig.setCustomHeaderPassword(customHeaderPassword); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getHeader(customHeaderUsername)).thenReturn(USERNAME); when(request.getHeader(customHeaderPassword)).thenReturn(PASSWORD); assertThat(filter.extractUsernamePassword(request)).containsExactly( USERNAME, PASSWORD); } }