package org.keycloak.testsuite.performance.httpclient; import org.apache.http.Header; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.graphene.page.Page; import org.jboss.logging.Logger; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Before; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; import org.keycloak.testsuite.performance.LoginLogoutTestParameters; import org.keycloak.testsuite.performance.OperationTimeoutException; import org.keycloak.testsuite.performance.PerformanceMeasurement; import org.keycloak.testsuite.performance.PerformanceTest; import org.keycloak.testsuite.performance.page.AppProfileJEE; import java.io.File; import java.io.IOException; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.net.HttpURLConnection.HTTP_OK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.keycloak.testsuite.performance.LoginLogoutTestParameters.ACCESS_REQUEST_TIME; import static org.keycloak.testsuite.performance.LoginLogoutTestParameters.LOGIN_REQUEST_TIME; import static org.keycloak.testsuite.performance.LoginLogoutTestParameters.LOGIN_VERIFY_REQUEST_TIME; import static org.keycloak.testsuite.performance.LoginLogoutTestParameters.LOGOUT_REQUEST_TIME; import static org.keycloak.testsuite.performance.LoginLogoutTestParameters.LOGOUT_VERIFY_REQUEST_TIME; import static org.keycloak.testsuite.performance.LoginLogoutTestParameters.PASSWORD_HASH_ITERATIONS; import static org.keycloak.testsuite.util.IOUtil.loadRealm; /** * * @author tkyjovsk */ @AppServerContainer("app-server-remote") public class HttpClientLoginLogoutPerfTest extends HttpClientPerformanceTest { private static final Logger LOG = Logger.getLogger(HttpClientLoginLogoutPerfTest.class); private static final String TEST_REALM = "Test"; private String securedUrl; private String logoutUrl; private String username; private String password; private String loginPageUrl; @Page protected AppProfileJEE appProfileJEEPage; protected static WebArchive warDeployment(String filename) throws IOException { return ShrinkWrap.createFromZipFile(WebArchive.class, new File(EXAMPLES_HOME + "/" + filename + ".war")) .addAsWebInfResource(jbossDeploymentStructure, JBOSS_DEPLOYMENT_STRUCTURE_XML); } @Deployment(name = AppProfileJEE.DEPLOYMENT_NAME) private static WebArchive appProfileJEE() throws IOException { return exampleDeployment("keycloak-test-app-profile-jee"); } @Override public void setDefaultPageUriParameters() { super.setDefaultPageUriParameters(); testRealmPage.setAuthRealm(TEST_REALM); } @Override public void addAdapterTestRealms(List<RealmRepresentation> testRealms) { RealmRepresentation examplesRealm = loadRealm("/test-realm.json"); examplesRealm.setPasswordPolicy("hashIterations(" + PASSWORD_HASH_ITERATIONS + ")"); testRealms.add(examplesRealm); } @Before public void beforeLoginLogoutTest() { securedUrl = appProfileJEEPage + "/profile.jsp"; logoutUrl = appProfileJEEPage + "/index.jsp?logout=true"; username = "secure-user"; password = "password"; loginPageUrl = testRealmLoginPage.toString(); } @Override public PerformanceTest.Runnable newRunnable() { return new Runnable(); } @Override protected boolean isMeasurementWithinLimits(PerformanceMeasurement measurement) { return LoginLogoutTestParameters.isMeasurementWithinLimits(measurement); } public class Runnable extends HttpClientPerformanceTest.Runnable { @Override public void performanceScenario() throws IOException, OperationTimeoutException { LOG.trace(String.format("Starting login-logout scenario #%s", getLoopCounter())); context.getCookieStore().clear(); // ACCESS String pageContent; final HttpGet getSecuredPageRequest = new HttpGet(securedUrl); LOG.trace(String.format("Accessing secured URL: %s", getSecuredPageRequest)); LOG.trace(getSecuredPageRequest); timer.reset(); try (CloseableHttpResponse r = client.execute(getSecuredPageRequest, context)) { assertEquals("ACCESS_REQUEST OK", HTTP_OK, r.getStatusLine().getStatusCode()); logRedirects(); assertEquals("ACCESS_REQUEST has 1 redirect", 1, context.getRedirectLocations().size()); assertTrue("ACCESS_REQUEST redirects to login page", getLastRedirect().toASCIIString().startsWith(loginPageUrl)); pageContent = EntityUtils.toString(r.getEntity()); } catch (SocketException ex) { throw new OperationTimeoutException(ACCESS_REQUEST_TIME, ex); } catch (SocketTimeoutException ex) { throw new OperationTimeoutException(ACCESS_REQUEST_TIME, ex.bytesTransferred, ex); } statistics.addValue(ACCESS_REQUEST_TIME, timer.getElapsedTime()); // LOGIN final HttpPost loginRequest = new HttpPost(getLoginUrlFromPage(pageContent)); List<NameValuePair> credentials = new ArrayList<>(); credentials.add(new BasicNameValuePair("username", username)); credentials.add(new BasicNameValuePair("password", password)); loginRequest.setEntity(new UrlEncodedFormEntity(credentials)); LOG.trace("Logging in"); LOG.trace(loginRequest); timer.reset(); try (CloseableHttpResponse r = client.execute(loginRequest, context)) { assertEquals("LOGIN_REQUEST OK", HTTP_OK, r.getStatusLine().getStatusCode()); logRedirects(); assertEquals("LOGIN_REQUEST has 2 redirects", 2, context.getRedirectLocations().size()); assertTrue("LOGIN_REQUEST redirects to secured page", getLastRedirect().toASCIIString().equals(securedUrl)); } catch (SocketException ex) { throw new OperationTimeoutException(LOGIN_REQUEST_TIME, ex); } catch (SocketTimeoutException ex) { throw new OperationTimeoutException(LOGIN_REQUEST_TIME, ex.bytesTransferred, ex); } statistics.addValue(LOGIN_REQUEST_TIME, timer.getElapsedTime()); // VERIFY LOGIN LOG.trace("Verifying login"); LOG.trace(getSecuredPageRequest); timer.reset(); try (CloseableHttpResponse r = client.execute(getSecuredPageRequest, context)) { assertEquals("LOGIN_VERIFY_REQUEST OK", HTTP_OK, r.getStatusLine().getStatusCode()); logRedirects(); assertEquals("LOGIN_VERIFY_REQUEST has 0 redirects", 0, context.getRedirectLocations().size()); } catch (SocketException ex) { throw new OperationTimeoutException(LOGIN_VERIFY_REQUEST_TIME, ex); } catch (SocketTimeoutException ex) { throw new OperationTimeoutException(LOGIN_VERIFY_REQUEST_TIME, ex.bytesTransferred, ex); } statistics.addValue(LOGIN_VERIFY_REQUEST_TIME, timer.getElapsedTime()); // LOGOUT final HttpGet logoutRequest = new HttpGet(logoutUrl); LOG.trace("Logging out"); LOG.trace(logoutRequest); timer.reset(); try (CloseableHttpResponse r = client.execute(logoutRequest, context)) { assertEquals("LOGOUT_REQUEST OK", HTTP_OK, r.getStatusLine().getStatusCode()); logRedirects(); assertEquals("LOGOUT_REQUEST has 0 redirects", 0, context.getRedirectLocations().size()); } catch (SocketException ex) { throw new OperationTimeoutException(LOGOUT_REQUEST_TIME, ex); } catch (SocketTimeoutException ex) { throw new OperationTimeoutException(LOGOUT_REQUEST_TIME, ex.bytesTransferred, ex); } statistics.addValue(LOGOUT_REQUEST_TIME, timer.getElapsedTime()); // VERIFY LOGOUT LOG.trace("Verifying logout"); LOG.trace(getSecuredPageRequest); timer.reset(); try (CloseableHttpResponse r = client.execute(getSecuredPageRequest, context)) { assertEquals("LOGOUT_VERIFY_REQUEST OK", HTTP_OK, r.getStatusLine().getStatusCode()); logRedirects(); assertEquals("LOGOUT_VERIFY_REQUEST has 1 redirect", 1, context.getRedirectLocations().size()); assertTrue("LOGOUT_VERIFY_REQUEST redirects to login page", getLastRedirect().toASCIIString().startsWith(loginPageUrl)); } catch (SocketException ex) { throw new OperationTimeoutException(LOGOUT_VERIFY_REQUEST_TIME, ex); } catch (SocketTimeoutException ex) { throw new OperationTimeoutException(LOGOUT_VERIFY_REQUEST_TIME, ex.bytesTransferred, ex); } statistics.addValue(LOGOUT_VERIFY_REQUEST_TIME, timer.getElapsedTime()); LOG.trace("Logged out"); } private URI getLastRedirect() { return context.getRedirectLocations().get(context.getRedirectLocations().size() - 1); } private void logRedirects() { int i = 0; for (URI uri : context.getRedirectLocations()) { LOG.trace(String.format("--> REDIRECT %s: %s", ++i, uri.toASCIIString())); } } public String getRedirectLocation(CloseableHttpResponse r) { Header locationHeader = r.getFirstHeader("Location"); assertNotNull(locationHeader); return locationHeader.getValue(); } private String getLoginUrlFromPage(String content) { String formActionRegex = "<form[^>]+action\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>"; Pattern p = Pattern.compile(formActionRegex); Matcher m = p.matcher(content); if (m.find()) { return m.group(1); } else { throw new IllegalStateException("Login url counldn't be parsed form page."); } } } }