/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.catalina.authenticator; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.http.HttpServletResponse; import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test; import org.apache.catalina.Context; import org.apache.catalina.Engine; import org.apache.catalina.Host; import org.apache.catalina.Service; import org.apache.catalina.connector.Request; import org.apache.catalina.core.StandardContext; import org.apache.catalina.core.StandardEngine; import org.apache.catalina.core.StandardHost; import org.apache.catalina.core.StandardService; import org.apache.catalina.filters.TesterHttpServletResponse; import org.apache.catalina.startup.TesterMapRealm; import org.apache.tomcat.util.descriptor.web.LoginConfig; import org.apache.tomcat.util.security.ConcurrentMessageDigest; import org.apache.tomcat.util.security.MD5Encoder; public class TesterDigestAuthenticatorPerformance { private static String USER = "user"; private static String PWD = "pwd"; private static String ROLE = "role"; private static String METHOD = "GET"; private static String URI = "/protected"; private static String CONTEXT_PATH = "/foo"; private static String CLIENT_AUTH_HEADER = "authorization"; private static String REALM = "TestRealm"; private static String QOP = "auth"; private static final AtomicInteger nonceCount = new AtomicInteger(0); private DigestAuthenticator authenticator = new DigestAuthenticator(); @Test public void testSimple() throws Exception { doTest(4, 1000000); } public void doTest(int threadCount, int requestCount) throws Exception { TesterRunnable runnables[] = new TesterRunnable[threadCount]; Thread threads[] = new Thread[threadCount]; String nonce = authenticator.generateNonce(new TesterDigestRequest()); // Create the runnables & threads for (int i = 0; i < threadCount; i++) { runnables[i] = new TesterRunnable(authenticator, nonce, requestCount); threads[i] = new Thread(runnables[i]); } long start = System.currentTimeMillis(); // Start the threads for (int i = 0; i < threadCount; i++) { threads[i].start(); } // Wait for the threads to finish for (int i = 0; i < threadCount; i++) { threads[i].join(); } double wallTime = System.currentTimeMillis() - start; // Gather the results... double totalTime = 0; int totalSuccess = 0; for (int i = 0; i < threadCount; i++) { System.out.println("Thread: " + i + " Success: " + runnables[i].getSuccess()); totalSuccess = totalSuccess + runnables[i].getSuccess(); totalTime = totalTime + runnables[i].getTime(); } System.out.println("Average time per request (user): " + totalTime/(threadCount * requestCount)); System.out.println("Average time per request (wall): " + wallTime/(threadCount * requestCount)); assertEquals(requestCount * threadCount, totalSuccess); } @Before public void setUp() throws Exception { ConcurrentMessageDigest.init("MD5"); // Configure the Realm TesterMapRealm realm = new TesterMapRealm(); realm.addUser(USER, PWD); realm.addUserRole(USER, ROLE); // Add the Realm to the Context Context context = new StandardContext(); context.setName(CONTEXT_PATH); context.setRealm(realm); Host host = new StandardHost(); context.setParent(host); Engine engine = new StandardEngine(); host.setParent(engine); Service service = new StandardService(); engine.setService(service); // Configure the Login config LoginConfig config = new LoginConfig(); config.setRealmName(REALM); context.setLoginConfig(config); // Make the Context and Realm visible to the Authenticator authenticator.setContainer(context); authenticator.setNonceCountWindowSize(8 * 1024); authenticator.start(); } private static class TesterRunnable implements Runnable { private String nonce; private int requestCount; private int success = 0; private long time = 0; private TesterDigestRequest request; private HttpServletResponse response; private DigestAuthenticator authenticator; private static final String A1 = USER + ":" + REALM + ":" + PWD; private static final String A2 = METHOD + ":" + CONTEXT_PATH + URI; private static final String MD5A1 = MD5Encoder.encode( ConcurrentMessageDigest.digest("MD5", A1.getBytes())); private static final String MD5A2 = MD5Encoder.encode( ConcurrentMessageDigest.digest("MD5", A2.getBytes())); // All init code should be in here. run() needs to be quick public TesterRunnable(DigestAuthenticator authenticator, String nonce, int requestCount) throws Exception { this.authenticator = authenticator; this.nonce = nonce; this.requestCount = requestCount; request = new TesterDigestRequest(); request.getMappingData().context = authenticator.context; response = new TesterHttpServletResponse(); } @Override public void run() { long start = System.currentTimeMillis(); for (int i = 0; i < requestCount; i++) { try { request.setAuthHeader(buildDigestResponse(nonce)); if (authenticator.authenticate(request, response)) { success++; } // Clear out authenticated user ready for next iteration request.setUserPrincipal(null); } catch (IOException ioe) { // Ignore } } time = System.currentTimeMillis() - start; } public int getSuccess() { return success; } public long getTime() { return time; } private String buildDigestResponse(String nonce) { String ncString = String.format("%1$08x", Integer.valueOf(nonceCount.incrementAndGet())); String cnonce = "cnonce"; String response = MD5A1 + ":" + nonce + ":" + ncString + ":" + cnonce + ":" + QOP + ":" + MD5A2; String md5response = MD5Encoder.encode( ConcurrentMessageDigest.digest("MD5", response.getBytes())); StringBuilder auth = new StringBuilder(); auth.append("Digest username=\""); auth.append(USER); auth.append("\", realm=\""); auth.append(REALM); auth.append("\", nonce=\""); auth.append(nonce); auth.append("\", uri=\""); auth.append(CONTEXT_PATH + URI); auth.append("\", opaque=\""); auth.append(authenticator.getOpaque()); auth.append("\", response=\""); auth.append(md5response); auth.append("\""); auth.append(", qop="); auth.append(QOP); auth.append(", nc="); auth.append(ncString); auth.append(", cnonce=\""); auth.append(cnonce); auth.append("\""); return auth.toString(); } } private static class TesterDigestRequest extends Request { private String authHeader = null; public TesterDigestRequest() { super(null); } @Override public String getRemoteAddr() { return "127.0.0.1"; } public void setAuthHeader(String authHeader) { this.authHeader = authHeader; } @Override public String getHeader(String name) { if (CLIENT_AUTH_HEADER.equalsIgnoreCase(name)) { return authHeader; } else { return super.getHeader(name); } } @Override public String getMethod() { return METHOD; } @Override public String getQueryString() { return null; } @Override public String getRequestURI() { return CONTEXT_PATH + URI; } @Override public org.apache.coyote.Request getCoyoteRequest() { return new org.apache.coyote.Request(); } } }