/******************************************************************************* * (C) Copyright 2014 Teknux.org (http://teknux.org/). * * 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. * * Contributors: * "Pierre PINON" * "Francois EYL" * "Laurent MARCHAL" * *******************************************************************************/ package org.teknux.jettybootstrap; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.util.ArrayList; import java.util.List; import org.apache.commons.io.FileUtils; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.webapp.WebAppContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.teknux.jettybootstrap.configuration.IJettyConfiguration; import org.teknux.jettybootstrap.configuration.JettyConnector; import org.teknux.jettybootstrap.configuration.PropertiesJettyConfiguration; import org.teknux.jettybootstrap.handler.ExplodedWarAppJettyHandler; import org.teknux.jettybootstrap.handler.JettyHandler; import org.teknux.jettybootstrap.handler.WarAppFromClasspathJettyHandler; import org.teknux.jettybootstrap.handler.WarAppJettyHandler; import org.teknux.jettybootstrap.keystore.JettyKeystoreConvertorBuilder; import org.teknux.jettybootstrap.keystore.JettyKeystoreException; import org.teknux.jettybootstrap.keystore.JettyKeystoreGeneratorBuilder; import org.teknux.jettybootstrap.utils.PathUtil; /** * Main class for easily boostrapping jetty. */ public class JettyBootstrap { private static final Logger LOG = LoggerFactory.getLogger(JettyBootstrap.class); private static final String DEFAULT_KEYSTORE_FILENAME = "default.keystore"; private static final String TEMP_DIRECTORY_NAME = ".temp"; public static final File TEMP_DIRECTORY_JARDIR = new File(PathUtil.getJarDir() + File.separator + TEMP_DIRECTORY_NAME); public static final File TEMP_DIRECTORY_SYSTEMP = new File(System.getProperty("java.io.tmpdir") + File.separator + TEMP_DIRECTORY_NAME); protected static final File TEMP_DIRECTORY_DEFAULT = TEMP_DIRECTORY_JARDIR; public static final String RESOURCE_WEBAPP = "/webapp"; public static final String CONTEXT_PATH_ROOT = "/"; private final IJettyConfiguration iJettyConfiguration; private boolean isInitializedConfiguration = false; private Server server = null; private final HandlerList handlers = new HandlerList(); /** * Shortcut to start Jetty when called within a JAR file containing the WEB-INF folder and needed libraries. * <p> * Basically uses {@link #addSelf()} and {@link #startServer()} * * @return a new instance of {@link JettyBootstrap} * @throws JettyBootstrapException * if an error occurs during the startup */ public static JettyBootstrap startSelf() throws JettyBootstrapException { final JettyBootstrap jettyBootstrap = new JettyBootstrap(); jettyBootstrap.addSelf(); return jettyBootstrap.startServer(); } /** * Default constructor using the default {@link PropertiesJettyConfiguration} configuration. */ public JettyBootstrap() { this(null); } /** * Constructor specifiying the configuration properties. * * @param iJettyconfiguration * the {@link IJettyConfiguration} implementation of the configuration */ public JettyBootstrap(final IJettyConfiguration iJettyconfiguration) { if (iJettyconfiguration == null) { this.iJettyConfiguration = new PropertiesJettyConfiguration(); } else { this.iJettyConfiguration = iJettyconfiguration.clone(); } } /** * Starts the Jetty Server and join the calling thread according to {@link IJettyConfiguration#isAutoJoinOnStart()} * * @return this instance * @throws JettyBootstrapException * if an exception occurs during the initialization * @see #startServer(Boolean) */ public JettyBootstrap startServer() throws JettyBootstrapException { return startServer(null); } /** * Starts the Jetty Server and join the calling thread. * * @param join * <code>true</code> to block the calling thread until the server stops. <code>false</code> otherwise * @return this instance * @throws JettyBootstrapException * if an exception occurs during the initialization */ public JettyBootstrap startServer(Boolean join) throws JettyBootstrapException { LOG.info("Starting Server..."); IJettyConfiguration iJettyConfiguration = getInitializedConfiguration(); initServer(iJettyConfiguration); try { server.start(); } catch (Exception e) { throw new JettyBootstrapException(e); } // display server addresses if (iJettyConfiguration.getJettyConnectors().contains(JettyConnector.HTTP)) { LOG.info("http://{}:{}", iJettyConfiguration.getHost(), iJettyConfiguration.getPort()); } if (iJettyConfiguration.getJettyConnectors().contains(JettyConnector.HTTPS)) { LOG.info("https://{}:{}", iJettyConfiguration.getHost(), iJettyConfiguration.getSslPort()); } if ((join != null && join) || (join == null && iJettyConfiguration.isAutoJoinOnStart())) { joinServer(); } return this; } /** * Blocks the calling thread until the server stops. * * @return this instance * @throws JettyBootstrapException * if an exception occurs while blocking the thread */ public JettyBootstrap joinServer() throws JettyBootstrapException { try { if (isServerStarted()) { LOG.debug("Joining Server..."); server.join(); } else { LOG.warn("Can't join Server. Not started"); } } catch (InterruptedException e) { throw new JettyBootstrapException(e); } return this; } /** * Return if server is started * * @return if server is started */ public boolean isServerStarted() { return (server != null && server.isStarted()); } /** * Stops the Jetty server. * * @return this instance * @throws JettyBootstrapException * if an exception occurs while stopping the server or if the server is not started */ public JettyBootstrap stopServer() throws JettyBootstrapException { LOG.info("Stopping Server..."); try { if (isServerStarted()) { handlers.stop(); server.stop(); LOG.info("Server stopped."); } else { LOG.warn("Can't stop server. Already stopped"); } } catch (Exception e) { throw new JettyBootstrapException(e); } return this; } /** * Add a War application the default context path {@value #CONTEXT_PATH_ROOT} * * @param war * the path to a war file * @return WebAppContext * @throws JettyBootstrapException * on failure */ public WebAppContext addWarApp(String war) throws JettyBootstrapException { return addWarApp(war, CONTEXT_PATH_ROOT); } /** * Add a War application specifying the context path. * * @param war * the path to a war file * @param contextPath * the path (base URL) to make the war available * @return WebAppContext * @throws JettyBootstrapException * on failure */ public WebAppContext addWarApp(String war, String contextPath) throws JettyBootstrapException { WarAppJettyHandler warAppJettyHandler = new WarAppJettyHandler(getInitializedConfiguration()); warAppJettyHandler.setWar(war); warAppJettyHandler.setContextPath(contextPath); WebAppContext webAppContext = warAppJettyHandler.getHandler(); handlers.addHandler(webAppContext); return webAppContext; } /** * Add a War application from the current classpath on the default context path {@value #CONTEXT_PATH_ROOT} * * @param warFromClasspath * the path to a war file in the classpath * @return WebAppContext * @throws JettyBootstrapException * on failed */ public WebAppContext addWarAppFromClasspath(String warFromClasspath) throws JettyBootstrapException { return addWarAppFromClasspath(warFromClasspath, CONTEXT_PATH_ROOT); } /** * Add a War application from the current classpath specifying the context path. * * @param warFromClasspath * the path to a war file in the classpath * @param contextPath * the path (base URL) to make the war available * @return WebAppContext * @throws JettyBootstrapException * on failed */ public WebAppContext addWarAppFromClasspath(String warFromClasspath, String contextPath) throws JettyBootstrapException { WarAppFromClasspathJettyHandler warAppFromClasspathJettyHandler = new WarAppFromClasspathJettyHandler(getInitializedConfiguration()); warAppFromClasspathJettyHandler.setWarFromClasspath(warFromClasspath); warAppFromClasspathJettyHandler.setContextPath(contextPath); WebAppContext webAppContext = warAppFromClasspathJettyHandler.getHandler(); handlers.addHandler(webAppContext); return webAppContext; } /** * Add an exploded (not packaged) War application on the default context path {@value #CONTEXT_PATH_ROOT} * * @param explodedWar * the exploded war path * @param descriptor * the web.xml descriptor path * @return WebAppContext * @throws JettyBootstrapException * on failed */ public WebAppContext addExplodedWarApp(String explodedWar, String descriptor) throws JettyBootstrapException { return addExplodedWarApp(explodedWar, descriptor, CONTEXT_PATH_ROOT); } /** * Add an exploded (not packaged) War application specifying the context path. * * @param explodedWar * the exploded war path * @param descriptor * the web.xml descriptor path * @param contextPath * the path (base URL) to make the resource available * @return WebAppContext * @throws JettyBootstrapException * on failed */ public WebAppContext addExplodedWarApp(String explodedWar, String descriptor, String contextPath) throws JettyBootstrapException { ExplodedWarAppJettyHandler explodedWarAppJettyHandler = new ExplodedWarAppJettyHandler(getInitializedConfiguration()); explodedWarAppJettyHandler.setWebAppBase(explodedWar); explodedWarAppJettyHandler.setDescriptor(descriptor); explodedWarAppJettyHandler.setContextPath(contextPath); WebAppContext webAppContext = explodedWarAppJettyHandler.getHandler(); handlers.addHandler(webAppContext); return webAppContext; } /** * Add an exploded (not packaged) War application from the current classpath, on the default context path {@value #CONTEXT_PATH_ROOT} * * @param explodedWar * the exploded war path * @return WebAppContext * @throws JettyBootstrapException * on failed */ public WebAppContext addExplodedWarAppFromClasspath(String explodedWar) throws JettyBootstrapException { return addExplodedWarAppFromClasspath(explodedWar, null); } /** * Add an exploded (not packaged) War application from the current classpath, on the default context path {@value #CONTEXT_PATH_ROOT} * * @param explodedWar * the exploded war path * @param descriptor * the web.xml descriptor path * @return WebAppContext * @throws JettyBootstrapException * on failed */ public WebAppContext addExplodedWarAppFromClasspath(String explodedWar, String descriptor) throws JettyBootstrapException { return addExplodedWarAppFromClasspath(explodedWar, descriptor, CONTEXT_PATH_ROOT); } /** * Add an exploded (not packaged) War application from the current classpath, specifying the context path. * * @param explodedWar * the exploded war path * @param descriptor * the web.xml descriptor path * @param contextPath * the path (base URL) to make the resource available * @return WebAppContext * @throws JettyBootstrapException * on failed */ public WebAppContext addExplodedWarAppFromClasspath(String explodedWar, String descriptor, String contextPath) throws JettyBootstrapException { ExplodedWarAppJettyHandler explodedWarAppJettyHandler = new ExplodedWarAppJettyHandler(getInitializedConfiguration()); explodedWarAppJettyHandler.setWebAppBaseFromClasspath(explodedWar); explodedWarAppJettyHandler.setDescriptor(descriptor); explodedWarAppJettyHandler.setContextPath(contextPath); WebAppContext webAppContext = explodedWarAppJettyHandler.getHandler(); handlers.addHandler(webAppContext); return webAppContext; } /** * Add an exploded War application found from {@value #RESOURCE_WEBAPP} in the current classpath on the default context path {@value #CONTEXT_PATH_ROOT} * * @see #addExplodedWarAppFromClasspath(String, String) * @return WebAppContext * @throws JettyBootstrapException * on failed */ public WebAppContext addSelf() throws JettyBootstrapException { return addExplodedWarAppFromClasspath(RESOURCE_WEBAPP, null); } /** * Add an exploded War application found from {@value #RESOURCE_WEBAPP} in the current classpath specifying the context path. * * @see #addExplodedWarAppFromClasspath(String, String, String) * @param contextPath * the path (base URL) to make the resource available * @return WebAppContext * @throws JettyBootstrapException * on failed */ public WebAppContext addSelf(String contextPath) throws JettyBootstrapException { return addExplodedWarAppFromClasspath(RESOURCE_WEBAPP, null, contextPath); } /** * Add Handler * * @param handler * Jetty Handler * @return Handler * @throws JettyBootstrapException * on failed */ public Handler addHandler(Handler handler) throws JettyBootstrapException { JettyHandler jettyHandler = new JettyHandler(); jettyHandler.setHandler(handler); handlers.addHandler(handler); return handler; } /** * Get the jetty {@link Server} Object. Calls {@link #initServer(IJettyConfiguration)} if not initialized yet. * * @return the contained jetty {@link Server} Object. * @throws JettyBootstrapException * if an error occurs during {@link #initServer(IJettyConfiguration)} */ public Server getServer() throws JettyBootstrapException { initServer(getInitializedConfiguration()); return server; } /** * Initialize Jetty server using the given {@link IJettyConfiguration}. Basically creates the server, set connectors, handlers and adds the shutdown hook. * * @param iJettyConfiguration * Jetty Configuration * @throws JettyBootstrapException * on failure */ protected void initServer(IJettyConfiguration iJettyConfiguration) throws JettyBootstrapException { if (server == null) { server = createServer(iJettyConfiguration); server.setConnectors(createConnectors(iJettyConfiguration, server)); server.setHandler(handlers); if (iJettyConfiguration.isStopAtShutdown()) { createShutdownHook(iJettyConfiguration); } } } /** * Parse the {@link IJettyConfiguration}, validate the configuration and initialize it if necessary. Clean temp directory if necessary and generates SSL keystore when * necessary. * * @return IJettyConfiguration Jetty Configuration * @throws JettyBootstrapException * on failure */ protected IJettyConfiguration getInitializedConfiguration() throws JettyBootstrapException { if (!isInitializedConfiguration) { LOG.debug("Init Configuration..."); LOG.trace("Check Temp Directory..."); if (iJettyConfiguration.getTempDirectory() == null) { iJettyConfiguration.setTempDirectory(TEMP_DIRECTORY_DEFAULT); } if (iJettyConfiguration.getTempDirectory().exists() && iJettyConfiguration.isCleanTempDir()) { LOG.trace("Clean Temp Directory..."); try { FileUtils.deleteDirectory(iJettyConfiguration.getTempDirectory()); } catch (IOException e) { throw new JettyBootstrapException("Can't clean temporary directory"); } } if (!iJettyConfiguration.getTempDirectory().exists() && !iJettyConfiguration.getTempDirectory().mkdirs()) { throw new JettyBootstrapException("Can't create temporary directory"); } LOG.trace("Check required properties..."); if (iJettyConfiguration.getHost() == null || iJettyConfiguration.getHost().isEmpty()) { throw new JettyBootstrapException("Host not specified"); } LOG.trace("Check connectors..."); if (iJettyConfiguration.hasJettyConnector(JettyConnector.HTTPS)) { //Checks keystore path only if keyStore object and SSL private key or SSL certificate are not specified if (iJettyConfiguration.getSslKeyStore() == null && (iJettyConfiguration.getSslPrivateKeyPath() == null || iJettyConfiguration.getSslPrivateKeyPath().isEmpty() || iJettyConfiguration.getSslCertificatePath() == null || iJettyConfiguration.getSslCertificatePath().isEmpty())) { //If keystore path is not specified, use default keystore path if (iJettyConfiguration.getSslKeyStorePath() == null || iJettyConfiguration.getSslKeyStorePath().isEmpty()) { iJettyConfiguration.setSslKeyStorePath(iJettyConfiguration.getTempDirectory().getPath() + File.separator + DEFAULT_KEYSTORE_FILENAME); } //Create keystore file if not exits File keystoreFile = new File(iJettyConfiguration.getSslKeyStorePath()); if (!keystoreFile.exists()) { try { JettyKeystoreGeneratorBuilder jettyKeystoreGeneratorBuilder = new JettyKeystoreGeneratorBuilder(); jettyKeystoreGeneratorBuilder.setAlgorithm(iJettyConfiguration.getSslKeyStoreAlgorithm()); jettyKeystoreGeneratorBuilder.setSignatureAlgorithm(iJettyConfiguration.getSslKeyStoreSignatureAlgorithm()); jettyKeystoreGeneratorBuilder.setRdnOuValue(iJettyConfiguration.getSslKeyStoreRdnOuValue()); jettyKeystoreGeneratorBuilder.setRdnOValue(iJettyConfiguration.getSslKeyStoreRdnOValue()); jettyKeystoreGeneratorBuilder.setDateNotBeforeNumberOfDays(iJettyConfiguration.getSslKeyStoreDateNotBeforeNumberOfDays()); jettyKeystoreGeneratorBuilder.setDateNotAfterNumberOfDays(iJettyConfiguration.getSslKeyStoreDateNotAfterNumberOfDays()); KeyStore keyStore = jettyKeystoreGeneratorBuilder.build(iJettyConfiguration.getSslKeyStoreDomainName(), iJettyConfiguration.getSslKeyStoreAlias(), iJettyConfiguration.getSslKeyStorePassword()); JettyKeystoreGeneratorBuilder.saveKeyStore(keyStore, keystoreFile, iJettyConfiguration.getSslKeyStorePassword()); } catch (JettyKeystoreException e) { throw new JettyBootstrapException("Can't generate keyStore", e); } } } } if (iJettyConfiguration.isRedirectWebAppsOnHttpsConnector() && (!iJettyConfiguration.hasJettyConnector(JettyConnector.HTTP) || !iJettyConfiguration.hasJettyConnector(JettyConnector.HTTPS))) { throw new JettyBootstrapException("You can't redirect all from HTTP to HTTPS Connector if both connectors are not setted"); } isInitializedConfiguration = true; LOG.trace("Configuration : {}", iJettyConfiguration); } return iJettyConfiguration; } /** * Convenient method used to build and return a new {@link Server}. * * @param iJettyConfiguration * Jetty Configuration * @return Server */ protected Server createServer(IJettyConfiguration iJettyConfiguration) { LOG.trace("Create Jetty Server..."); Server server = new Server(new QueuedThreadPool(iJettyConfiguration.getMaxThreads())); server.setStopAtShutdown(false); // Reimplemented. See // @IJettyConfiguration.stopAtShutdown server.setStopTimeout(iJettyConfiguration.getStopTimeout()); return server; } /** * Creates and returns the necessary {@link ServerConnector} based on the given {@link IJettyConfiguration}. * * @param iJettyConfiguration * Jetty Configuration * @param server * the server to * @return Connector[] * @throws JettyBootstrapException */ protected Connector[] createConnectors(IJettyConfiguration iJettyConfiguration, Server server) throws JettyBootstrapException { LOG.trace("Creating Jetty Connectors..."); List<Connector> connectors = new ArrayList<Connector>(); if (iJettyConfiguration.hasJettyConnector(JettyConnector.HTTP)) { LOG.trace("Adding HTTP Connector..."); ServerConnector serverConnector; if (iJettyConfiguration.hasJettyConnector(JettyConnector.HTTPS)) { HttpConfiguration httpConfiguration = new HttpConfiguration(); httpConfiguration.setSecurePort(iJettyConfiguration.getSslPort()); httpConfiguration.setSecureScheme(HttpScheme.HTTPS.asString()); HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfiguration); serverConnector = new ServerConnector(server, httpConnectionFactory); } else { serverConnector = new ServerConnector(server); } serverConnector.setIdleTimeout(iJettyConfiguration.getIdleTimeout()); serverConnector.setHost(iJettyConfiguration.getHost()); serverConnector.setPort(iJettyConfiguration.getPort()); connectors.add(serverConnector); } if (iJettyConfiguration.hasJettyConnector(JettyConnector.HTTPS)) { LOG.trace("Adding HTTPS Connector..."); SslContextFactory sslContextFactory = new SslContextFactory(); if (iJettyConfiguration.getSslKeyStore() != null) { //Use keyStore object if available sslContextFactory.setKeyStore(iJettyConfiguration.getSslKeyStore()); } else if (iJettyConfiguration.getSslPrivateKeyPath() != null && !iJettyConfiguration.getSslPrivateKeyPath().isEmpty() && iJettyConfiguration.getSslCertificatePath() != null && !iJettyConfiguration.getSslCertificatePath().isEmpty()) { //Use private key and certificate if available JettyKeystoreConvertorBuilder jettyKeystoreConvertorBuilder = new JettyKeystoreConvertorBuilder(); try { File sslPrivateKeyFile = new File(iJettyConfiguration.getSslPrivateKeyPath()); if (!sslPrivateKeyFile.exists() || !sslPrivateKeyFile.canRead()) { throw new JettyBootstrapException("Private key not exists or unreadable"); } File sslCertificateFile = new File(iJettyConfiguration.getSslCertificatePath()); if (!sslCertificateFile.exists() || !sslCertificateFile.canRead()) { throw new JettyBootstrapException("Certificate not exists or unreadable"); } try (InputStream sslPrivateKeyInputStream = new FileInputStream(sslPrivateKeyFile); InputStream sslCertificateInputStream = new FileInputStream(sslCertificateFile)) { switch (iJettyConfiguration.getSslCertificateFormat()) { case PKCS8: jettyKeystoreConvertorBuilder.setCertificateFromPKCS8(sslCertificateInputStream); break; case PKCS12: jettyKeystoreConvertorBuilder.setCertificateFromPKCS12(sslCertificateInputStream, iJettyConfiguration.getSslCertificatePassword()); break; case UNKNOWN: throw new JettyBootstrapException("Unknown Certificate Format"); default: throw new JettyBootstrapException("Certificate Format not setted"); } switch (iJettyConfiguration.getSslPrivateKeyFormat()) { case PKCS8: jettyKeystoreConvertorBuilder.setPrivateKeyFromPKCS8(sslPrivateKeyInputStream); break; case PKCS12: jettyKeystoreConvertorBuilder.setPrivateKeyFromPKCS12(sslPrivateKeyInputStream, iJettyConfiguration.getSslPrivateKeyPassword()); break; case UNKNOWN: throw new JettyBootstrapException("Unknown Private key Format"); default: throw new JettyBootstrapException("Private key Format not setted"); } } KeyStore keyStore = jettyKeystoreConvertorBuilder.build(iJettyConfiguration.getSslKeyStoreAlias(), iJettyConfiguration.getSslKeyStorePassword()); sslContextFactory.setKeyStore(keyStore); } catch (JettyKeystoreException | IOException e) { throw new JettyBootstrapException("Can not load SSL private key or SSL certificate", e); } } else { //Use keystore path sslContextFactory.setKeyStorePath(iJettyConfiguration.getSslKeyStorePath()); } sslContextFactory.setKeyStorePassword(iJettyConfiguration.getSslKeyStorePassword()); ServerConnector serverConnector = new ServerConnector(server, sslContextFactory); serverConnector.setIdleTimeout(iJettyConfiguration.getIdleTimeout()); serverConnector.setHost(iJettyConfiguration.getHost()); serverConnector.setPort(iJettyConfiguration.getSslPort()); connectors.add(serverConnector); } return connectors.toArray(new Connector[connectors.size()]); } /** * Create Shutdown Hook. * * @param iJettyConfiguration * Jetty Configuration */ private void createShutdownHook(final IJettyConfiguration iJettyConfiguration) { LOG.trace("Creating Jetty ShutdownHook..."); Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { try { LOG.debug("Shutting Down..."); stopServer(); } catch (Exception e) { LOG.error("Shutdown", e); } } }); } }