001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.testing.web;
020
021import org.apache.shiro.codec.Base64;
022
023import com.gargoylesoftware.htmlunit.WebClient;
024import com.github.mjeanroy.junit.servers.jetty.EmbeddedJetty;
025import com.github.mjeanroy.junit.servers.jetty.EmbeddedJettyConfiguration;
026import org.eclipse.jetty.annotations.AnnotationConfiguration;
027import org.eclipse.jetty.http.HttpVersion;
028import org.eclipse.jetty.server.HttpConfiguration;
029import org.eclipse.jetty.server.HttpConnectionFactory;
030import org.eclipse.jetty.server.SecureRequestCustomizer;
031import org.eclipse.jetty.server.Server;
032import org.eclipse.jetty.server.ServerConnector;
033import org.eclipse.jetty.server.SslConnectionFactory;
034import org.eclipse.jetty.util.resource.FileResource;
035import org.eclipse.jetty.util.ssl.SslContextFactory;
036import org.eclipse.jetty.webapp.Configuration;
037import org.eclipse.jetty.webapp.FragmentConfiguration;
038import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
039import org.eclipse.jetty.webapp.MetaInfConfiguration;
040import org.eclipse.jetty.webapp.WebAppContext;
041import org.eclipse.jetty.webapp.WebInfConfiguration;
042import org.eclipse.jetty.webapp.WebXmlConfiguration;
043import org.junit.AfterClass;
044import org.junit.Before;
045import org.junit.BeforeClass;
046
047import java.io.File;
048import java.io.FilenameFilter;
049import java.io.IOException;
050import java.io.UnsupportedEncodingException;
051import java.net.ServerSocket;
052import java.net.URL;
053import java.nio.file.Files;
054import java.nio.file.Path;
055import java.nio.file.StandardCopyOption;
056
057import static com.github.mjeanroy.junit.servers.commons.Strings.isNotBlank;
058import static org.eclipse.jetty.util.resource.Resource.newResource;
059import static org.junit.Assert.assertEquals;
060import static org.junit.Assert.assertTrue;
061
062public abstract class AbstractContainerIT {
063
064    protected static EmbeddedJetty jetty;
065
066    protected static int tlsPort;
067
068    protected final WebClient webClient = new WebClient();
069
070    protected static final File TEST_KEYSTORE_PATH = setupKeyStore();
071    protected static final String TEST_KEYSTORE_PASSWORD = "password";
072
073    @BeforeClass
074    public static void startContainer() throws Exception {
075
076        EmbeddedJettyConfiguration config = EmbeddedJettyConfiguration.builder()
077                .withWebapp(getWarDir())
078                .build();
079
080        jetty = new EmbeddedJetty(config) {
081
082            /**
083             * Overriding with contents of this pull request, to make fragment scanning work.
084             * https://github.com/mjeanroy/junit-servers/pull/3
085             */
086            protected WebAppContext createdWebAppContext() throws Exception {
087                final String path = configuration.getPath();
088                final String webapp = configuration.getWebapp();
089                final String classpath = configuration.getClasspath();
090
091                WebAppContext ctx = new WebAppContext();
092                ctx.setClassLoader(Thread.currentThread().getContextClassLoader());
093                ctx.setContextPath(path);
094
095                // Useful for WebXmlConfiguration
096                ctx.setBaseResource(newResource(webapp));
097
098                ctx.setConfigurations(new Configuration[]{
099                        new WebInfConfiguration(),
100                        new WebXmlConfiguration(),
101                        new AnnotationConfiguration(),
102                        new JettyWebXmlConfiguration(),
103                        new MetaInfConfiguration(),
104                        new FragmentConfiguration(),
105                });
106
107                if (isNotBlank(classpath)) {
108                    // Fix to scan Spring WebApplicationInitializer
109                    // This will add compiled classes to jetty classpath
110                    // See: http://stackoverflow.com/questions/13222071/spring-3-1-webapplicationinitializer-embedded-jetty-8-annotationconfiguration
111                    // And more precisely: http://stackoverflow.com/a/18449506/1215828
112                    File classes = new File(classpath);
113                    FileResource containerResources = new FileResource(classes.toURI());
114                    ctx.getMetaData().addContainerResource(containerResources);
115                }
116
117                Server server = getDelegate();
118
119                // web app
120                ctx.setParentLoaderPriority(true);
121                ctx.setWar(webapp);
122                ctx.setServer(server);
123
124                // Add server context
125                server.setHandler(ctx);
126
127                return ctx;
128            }
129        };
130
131        Server server = jetty.getDelegate();
132
133        // TLS
134        tlsPort = getFreePort();
135
136        final SslContextFactory sslContextFactory = new SslContextFactory.Server();
137        sslContextFactory.setKeyStorePath(TEST_KEYSTORE_PATH.getAbsolutePath());
138        sslContextFactory.setKeyStorePassword(TEST_KEYSTORE_PASSWORD);
139        sslContextFactory.setKeyManagerPassword(TEST_KEYSTORE_PASSWORD);
140
141        HttpConfiguration https = new HttpConfiguration();
142        https.addCustomizer(new SecureRequestCustomizer());
143
144        final ServerConnector httpsConnector = new ServerConnector(
145                server,
146                new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
147                new HttpConnectionFactory(https));
148        httpsConnector.setPort(tlsPort);
149        server.addConnector(httpsConnector);
150
151        jetty.start();
152
153        assertTrue(jetty.isStarted());
154    }
155
156    protected static String getBaseUri() {
157        return "http://localhost:" + jetty.getPort() + "/";
158    }
159
160    protected static String getTlsBaseUri() {
161        return "https://localhost:" + tlsPort + "/";
162    }
163
164    protected static String getWarDir() {
165        File[] warFiles = new File("target").listFiles(new FilenameFilter() {
166            @Override
167            public boolean accept(File dir, String name) {
168                return name.endsWith(".war");
169            }
170        });
171
172        assertEquals("Expected only one war file in target directory, run 'mvn clean' and try again", 1, warFiles.length);
173
174        return warFiles[0].getAbsolutePath().replaceFirst("\\.war$", "");
175    }
176
177    protected static String getBasicAuthorizationHeaderValue(String username, String password) throws UnsupportedEncodingException {
178        String authorizationHeader = username + ":" + password;
179        byte[] valueBytes;
180        valueBytes = authorizationHeader.getBytes("UTF-8");
181        authorizationHeader = new String(Base64.encode(valueBytes));
182        return "Basic " + authorizationHeader;
183    }
184
185    @Before
186    public void beforeTest() {
187        webClient.getOptions().setThrowExceptionOnFailingStatusCode(true);
188    }
189
190    @AfterClass
191    public static void stopContainer() {
192        if (jetty != null) {
193            jetty.stop();
194        }
195    }
196
197    private static int getFreePort() {
198        try (ServerSocket socket = new ServerSocket(0)) {
199            return socket.getLocalPort();
200        } catch (IOException e) {
201            throw new IllegalStateException("Failed to allocate free port", e);
202        }
203    }
204
205    // Dealing with a keystore is NOT fun, it's easier to script one with the keytool
206    // see src/main/resources/createKeyStore.sh for more info
207    private static File setupKeyStore() {
208        try {
209            Path outKeyStoreFile = File.createTempFile("test-keystore", "jks").toPath();
210            URL keyStoreResource = Thread.currentThread().getContextClassLoader().getResource("test-keystore.jks");
211            Files.copy(keyStoreResource.openStream(), outKeyStoreFile, StandardCopyOption.REPLACE_EXISTING);
212            File keyStoreFile = outKeyStoreFile.toFile();
213
214            // clients will pick up the ssl keystore this way, so just set SSL properties
215            System.setProperty("javax.net.ssl.trustStore", keyStoreFile.getAbsolutePath());
216            System.setProperty("javax.net.ssl.trustStorePassword", TEST_KEYSTORE_PASSWORD);
217            return keyStoreFile;
218        } catch (IOException e) {
219            throw new IllegalStateException("Failed to create test keystore", e);
220        }
221    }
222}