/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * Licensed 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. * #L% */ package org.wisdom.framework.vertx; import com.google.common.collect.ImmutableMap; import org.apache.commons.io.IOUtils; import org.apache.http.client.CookieStore; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.junit.After; import org.junit.Test; import org.wisdom.api.Controller; import org.wisdom.api.DefaultController; import org.wisdom.api.configuration.ApplicationConfiguration; import org.wisdom.api.cookies.Cookie; import org.wisdom.api.cookies.SessionCookie; import org.wisdom.api.crypto.Crypto; import org.wisdom.api.exceptions.ExceptionMapper; import org.wisdom.api.http.HttpMethod; import org.wisdom.api.http.Request; import org.wisdom.api.http.Result; import org.wisdom.api.router.Route; import org.wisdom.api.router.RouteBuilder; import org.wisdom.api.router.Router; import org.wisdom.api.utils.CookieDataCodec; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; /** * Check the wisdom server behavior. * This class is listening for http requests on random port. */ public class CookiesTest extends VertxBaseTest { private WisdomVertxServer server; @After public void tearDown() { if (server != null) { server.stop(); server = null; } } @Test public void testCookie() throws InterruptedException, IOException { Router router = prepareServer(); // Prepare the router with a controller Controller controller = new DefaultController() { @SuppressWarnings("unused") public Result index() { if (context().parameter("id") == null) { System.err.println("No param: " + context().parameters()); } return ok("Alright").with(Cookie.builder("my-cookie", context().parameter("id")).setMaxAge(1000) .build()); } @SuppressWarnings("unused") public Result logged() { String id = context().cookieValue("my-cookie"); if (id == null) { return badRequest("no cookie"); } else { return ok(id); } } }; final Route route1 = new RouteBuilder().route(HttpMethod.GET) .on("/") .to(controller, "index"); final Route route2 = new RouteBuilder().route(HttpMethod.GET) .on("/logged") .to(controller, "logged"); configureRouter(router, route1, route2); server.start(); waitForStart(server); // Now start bunch of clients int num = NUMBER_OF_CLIENTS; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(num); int port = server.httpPort(); for (int i = 0; i < num; ++i) {// create and start threads executor.submit(new LoggedClient(startSignal, doneSignal, port, i, false)); } startSignal.countDown(); // let all threads proceed assertThat(doneSignal.await(60, TimeUnit.SECONDS)).isTrue(); // wait for all to finish assertThat(failure).isEmpty(); assertThat(success).hasSize(num); } private void configureRouter(Router router, Route root, Route logged) { doAnswer(invocationOnMock -> { String path = (String) invocationOnMock.getArguments()[1]; if (path.equals("/")) { return root; } if (path.equals("/logged")) { return logged; } return null; }).when(router).getRouteFor(anyString(), anyString(), any(Request.class)); } @Test public void testCookieContainingComplexValues() throws InterruptedException, IOException { Router router = prepareServer(); // Prepare the router with a controller Controller controller = new DefaultController() { @SuppressWarnings("unused") public Result index() throws UnsupportedEncodingException { if (context().parameter("id") == null) { System.err.println("No param: " + context().parameters()); } return ok("Alright").with(Cookie.builder("my-cookie", CookieDataCodec.encode(ImmutableMap.of("key1", "val1", "key2", "val2", "id", context().parameter("id")))).setMaxAge(1000).build()); } @SuppressWarnings("unused") public Result logged() throws UnsupportedEncodingException { String encoded = context().cookieValue("my-cookie"); Map<String, String> map = new LinkedHashMap<>(); CookieDataCodec.decode(map, encoded); // Check contained values if (map.get("id") == null) { return badRequest("cannot find id in " + map); } if (!"val1".equalsIgnoreCase(map.get("key1"))) { return badRequest("cannot find key1 in " + map); } if (!"val2".equalsIgnoreCase(map.get("key2"))) { return badRequest("cannot find key2 in " + map); } return ok(map.get("id")); } }; final Route route1 = new RouteBuilder().route(HttpMethod.GET) .on("/") .to(controller, "index"); final Route route2 = new RouteBuilder().route(HttpMethod.GET) .on("/logged") .to(controller, "logged"); configureRouter(router, route1, route2); server.start(); waitForStart(server); // Now start bunch of clients int num = NUMBER_OF_CLIENTS; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(num); int port = server.httpPort(); for (int i = 0; i < num; ++i) {// create and start threads executor.submit(new LoggedClient(startSignal, doneSignal, port, i, true)); } startSignal.countDown(); // let all threads proceed assertThat(doneSignal.await(60, TimeUnit.SECONDS)).isTrue(); // wait for all to finish assertThat(failure).isEmpty(); assertThat(success).hasSize(num); } @Test public void testTwoCookies() throws InterruptedException, IOException { Router router = prepareServer(); // Prepare the router with a controller Controller controller = new DefaultController() { @SuppressWarnings("unused") public Result index() { return ok("Alright") .with(Cookie.builder("my-cookie", context().parameter("id")).setMaxAge(1000).build()) .with(Cookie.builder("my-cookie-2", context().parameter("id")).setMaxAge(1000).build()); } @SuppressWarnings("unused") public Result logged() { String id = context().cookieValue("my-cookie"); String id2 = context().cookie("my-cookie-2").value(); if (id == null || id2 == null) { return badRequest("no cookie"); } else { return ok(id); } } }; final Route route1 = new RouteBuilder().route(HttpMethod.GET) .on("/") .to(controller, "index"); final Route route2 = new RouteBuilder().route(HttpMethod.GET) .on("/logged") .to(controller, "logged"); configureRouter(router, route1, route2); server.start(); waitForStart(server); // Now start bunch of clients int num = NUMBER_OF_CLIENTS; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(num); int port = server.httpPort(); for (int i = 0; i < num; ++i) // create and start threads executor.submit(new LoggedClient(startSignal, doneSignal, port, i, false)); startSignal.countDown(); // let all threads proceed doneSignal.await(30, TimeUnit.SECONDS); // wait for all to finish assertThat(failure).isEmpty(); assertThat(success).hasSize(num); } @Test public void testSession() throws InterruptedException, IOException { Router router = prepareServer(); // Prepare the router with a controller Controller controller = new DefaultController() { @SuppressWarnings("unused") public Result index() { context().session().put("id", context().parameter("id")); return ok("Alright"); } @SuppressWarnings("unused") public Result logged() { String id = context().session().get("id"); if (id == null) { return badRequest("no session"); } else { return ok(id); } } }; final Route route1 = new RouteBuilder().route(HttpMethod.GET) .on("/") .to(controller, "index"); final Route route2 = new RouteBuilder().route(HttpMethod.GET) .on("/logged") .to(controller, "logged"); configureRouter(router, route1, route2); server.start(); waitForStart(server); // Now start bunch of clients int num = 1; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(num); int port = server.httpPort(); for (int i = 0; i < num; ++i) { executor.submit(new LoggedClient(startSignal, doneSignal, port, i, true)); } startSignal.countDown(); // let all threads proceed doneSignal.await(60, TimeUnit.SECONDS); // wait for all to finish assertThat(failure).isEmpty(); assertThat(success).hasSize(num); } @Test public void testFlash() throws InterruptedException, IOException { Router router = prepareServer(); // Prepare the router with a controller Controller controller = new DefaultController() { @SuppressWarnings("unused") public Result index() { context().flash().put("id", context().parameter("id")); return ok("Alright"); } @SuppressWarnings("unused") public Result logged() { String id = context().flash().get("id"); if (id == null) { return badRequest("no flash"); } else { return ok(id); } } }; final Route route1 = new RouteBuilder().route(HttpMethod.GET) .on("/") .to(controller, "index"); final Route route2 = new RouteBuilder().route(HttpMethod.GET) .on("/logged") .to(controller, "logged"); configureRouter(router, route1, route2); server.start(); waitForStart(server); // Now start bunch of clients int num = NUMBER_OF_CLIENTS; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(num); int port = server.httpPort(); for (int i = 0; i < num; ++i) { // create and start threads executor.submit(new LoggedClient(startSignal, doneSignal, port, i, true)); } startSignal.countDown(); // let all threads proceed doneSignal.await(30, TimeUnit.SECONDS); // wait for all to finish assertThat(failure).isEmpty(); assertThat(success).hasSize(num); } private Router prepareServer() { // Prepare the configuration ApplicationConfiguration configuration = mock(ApplicationConfiguration.class); when(configuration.getIntegerWithDefault(eq("vertx.http.port"), anyInt())).thenReturn(0); when(configuration.getIntegerWithDefault(eq("vertx.https.port"), anyInt())).thenReturn(-1); when(configuration.getIntegerWithDefault("request.body.max.size", 100 * 1024)).thenReturn(100 * 1024); when(configuration.getIntegerWithDefault("vertx.acceptBacklog", -1)).thenReturn(-1); when(configuration.getIntegerWithDefault("vertx.receiveBufferSize", -1)).thenReturn(-1); when(configuration.getIntegerWithDefault("vertx.sendBufferSize", -1)).thenReturn(-1); when(configuration.getWithDefault(Cookie.APPLICATION_COOKIE_PREFIX, "wisdom")).thenReturn("wisdom"); when(configuration.getIntegerWithDefault(SessionCookie.SESSION_EXPIRE_TIME_SECOND, 3600) * 1000).thenReturn(1000); when(configuration.getBooleanWithDefault(SessionCookie.SESSION_SEND_ONLY_IF_CHANGED, true)).thenReturn(true); when(configuration.getBooleanWithDefault(SessionCookie.SESSION_OVER_HTTPS_ONLY, false)).thenReturn(false); when(configuration.getBooleanWithDefault(SessionCookie.SESSION_HTTP_ONLY, true)).thenReturn(true); when(configuration.getStringArray("wisdom.websocket.subprotocols")).thenReturn(new String[0]); when(configuration.getStringArray("vertx.websocket-subprotocols")).thenReturn(new String[0]); Crypto crypto = mock(Crypto.class); when(crypto.sign(anyString())).thenReturn("aaaaaa"); Router router = mock(Router.class); // Configure the server. server = new WisdomVertxServer(); server.configuration = configuration; server.accessor = new ServiceAccessor( crypto, configuration, router, getMockContentEngine(), executor, null, Collections.<ExceptionMapper>emptyList() ); server.vertx = vertx; return router; } @Test public void testThatCookiesAreWithdrawnCorrectly() throws InterruptedException, IOException { Router router = prepareServer(); // Prepare the router with a controller Controller controller = new DefaultController() { @SuppressWarnings("unused") public Result index() { if (context().parameter("id") == null) { return badRequest("'id' parameter required"); } return ok("Alright").with(Cookie.builder("my-cookie", context().parameter("id")).setMaxAge(3600) .setSecure(false).build()); } @SuppressWarnings("unused") public Result logged() { String id = context().cookieValue("my-cookie"); if (id == null) { return badRequest("no cookie"); } else { return ok(id).without("my-cookie"); } } }; final Route route1 = new RouteBuilder().route(HttpMethod.GET) .on("/") .to(controller, "index"); final Route route2 = new RouteBuilder().route(HttpMethod.GET) .on("/logged") .to(controller, "logged"); configureRouter(router, route1, route2); server.start(); waitForStart(server); // Now start bunch of clients int num = NUMBER_OF_CLIENTS; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(num); int port = server.httpPort(); for (int i = 0; i < num; ++i) {// create and start threads final LoggedClient task = new LoggedClient(startSignal, doneSignal, port, i, false) .additionalChecks((context, response, content) -> context.getCookieStore().getCookies().isEmpty()); executor.submit(task); } startSignal.countDown(); // let all threads proceed assertThat(doneSignal.await(60, TimeUnit.SECONDS)).isTrue(); // wait for all to finish assertThat(failure).isEmpty(); assertThat(success).hasSize(num); } @Test public void testThatCookiesCanBeReplaced() throws InterruptedException, IOException { Router router = prepareServer(); // Prepare the router with a controller Controller controller = new DefaultController() { @SuppressWarnings("unused") public Result index() { if (context().parameter("id") == null) { return badRequest("'id' parameter required"); } return ok("Alright").with(Cookie.builder("my-cookie", context().parameter("id")).setMaxAge(3600) .setSecure(false).build()); } @SuppressWarnings("unused") public Result logged() { String id = context().cookieValue("my-cookie"); if (id == null) { return badRequest("no cookie"); } else { return ok(id).with(Cookie.cookie("my-cookie", id + "_").build()); } } }; final Route route1 = new RouteBuilder().route(HttpMethod.GET) .on("/") .to(controller, "index"); final Route route2 = new RouteBuilder().route(HttpMethod.GET) .on("/logged") .to(controller, "logged"); configureRouter(router, route1, route2); server.start(); waitForStart(server); // Now start bunch of clients int num = NUMBER_OF_CLIENTS; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(num); int port = server.httpPort(); createAndSubmitClients(num, startSignal, doneSignal, port); startSignal.countDown(); // let all threads proceed assertThat(doneSignal.await(60, TimeUnit.SECONDS)).isTrue(); // wait for all to finish assertThat(failure).isEmpty(); assertThat(success).hasSize(num); } private void createAndSubmitClients(int num, CountDownLatch startSignal, CountDownLatch doneSignal, int port) { for (int i = 0; i < num; ++i) {// create and start threads final int id = i; final LoggedClient task = new LoggedClient(startSignal, doneSignal, port, i, false) .additionalChecks((context, response, content) -> { final org.apache.http.cookie.Cookie cookie = context.getCookieStore().getCookies().get(0); return cookie.getValue().equals(id + "_"); }); executor.submit(task); } } @Test public void testThatCookiesCanBeWithdrawnAndReplaced() throws InterruptedException, IOException { Router router = prepareServer(); // Prepare the router with a controller Controller controller = new DefaultController() { @SuppressWarnings("unused") public Result index() { if (context().parameter("id") == null) { return badRequest("'id' parameter required"); } return ok("Alright").with(Cookie.builder("my-cookie", context().parameter("id")).setMaxAge(3600) .setSecure(false).build()); } @SuppressWarnings("unused") public Result logged() { String id = context().cookieValue("my-cookie"); if (id == null) { return badRequest("no cookie"); } else { return ok(id).without("my-cookie").with(Cookie.cookie("my-cookie", id + "_").build()); } } }; final Route route1 = new RouteBuilder().route(HttpMethod.GET) .on("/") .to(controller, "index"); final Route route2 = new RouteBuilder().route(HttpMethod.GET) .on("/logged") .to(controller, "logged"); configureRouter(router, route1, route2); server.start(); waitForStart(server); // Now start bunch of clients int num = NUMBER_OF_CLIENTS; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(num); int port = server.httpPort(); createAndSubmitClients(num, startSignal, doneSignal, port); startSignal.countDown(); // let all threads proceed assertThat(doneSignal.await(60, TimeUnit.SECONDS)).isTrue(); // wait for all to finish assertThat(failure).isEmpty(); assertThat(success).hasSize(num); } private class LoggedClient implements Runnable { private final CountDownLatch startSignal; private final CountDownLatch doneSignal; private final int port; private final int id; private final boolean noCheck; private Checker additionalChecks; LoggedClient(CountDownLatch startSignal, CountDownLatch doneSignal, int port, int id, boolean noCheck) { this.startSignal = startSignal; this.doneSignal = doneSignal; this.port = port; this.id = id; this.noCheck = noCheck; } public LoggedClient additionalChecks(Checker checks) { this.additionalChecks = checks; return this; } public void run() { try { startSignal.await(); doWork(); } catch (Throwable ex) { ex.printStackTrace(); fail(id); } doneSignal.countDown(); } void doWork() throws IOException { CloseableHttpClient httpclient = null; CloseableHttpResponse response = null; try { RequestConfig globalConfig = RequestConfig.custom() .setCookieSpec(CookieSpecs.STANDARD) .build(); httpclient = HttpClients.custom() .setDefaultRequestConfig(globalConfig) .build(); CookieStore cookieStore = new BasicCookieStore(); HttpClientContext context = HttpClientContext.create(); context.setCookieStore(cookieStore); final String uri = "http://localhost:" + port + "/?id=" + id; HttpGet req1 = new HttpGet(uri); response = httpclient.execute(req1, context); String content = EntityUtils.toString(response.getEntity()); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(content).isEqualTo("Alright"); if (!noCheck) { org.apache.http.cookie.Cookie c = context.getCookieStore().getCookies().get(0); if (!c.getName().equalsIgnoreCase("my-cookie")) { System.err.println("Wrong cookie name for " + id); fail(id); return; } if (!c.getValue().equalsIgnoreCase(String.valueOf(id))) { System.err.println("Wrong cookie content for " + id); fail(id); return; } } // Proceed to the second request. HttpGet req2 = new HttpGet("http://localhost:" + port + "/logged"); response = httpclient.execute(req2, context); content = EntityUtils.toString(response.getEntity()); if (!isOk(response)) { System.err.println("Bad status code for " + id + " : " + content); fail(id); return; } if (!content.equalsIgnoreCase(String.valueOf(id))) { System.err.println("Wrong response content for " + id); fail(id); return; } if (additionalChecks != null) { try { if (additionalChecks.check(context, response, content)) { success(id); } else { fail(id); } } catch (Exception e) { System.err.println("Additional checks have thrown an exception for " + id); e.printStackTrace(); fail(id); } } else { success(id); } } finally { IOUtils.closeQuietly(httpclient); IOUtils.closeQuietly(response); } } } public interface Checker { boolean check(HttpClientContext context, CloseableHttpResponse response, String content) throws Exception; } }