/*
* Copyright (C) 2015 the original author or authors.
*
* 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.
*/
package ro.pippo.undertow;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.Undertow.Builder;
import io.undertow.UndertowOptions;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.server.handlers.PathHandler;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.FilterInfo;
import io.undertow.servlet.api.ListenerInfo;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.handlers.DefaultServlet;
import io.undertow.servlet.util.ImmediateInstanceFactory;
import org.kohsuke.MetaInfServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.pippo.core.AbstractWebServer;
import ro.pippo.core.Application;
import ro.pippo.core.PippoFilter;
import ro.pippo.core.PippoRuntimeException;
import ro.pippo.core.PippoServletContextListener;
import ro.pippo.core.WebServer;
import ro.pippo.undertow.websocket.UndertowWebSocketFilter;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.servlet.DispatcherType;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
/**
* An implementation of WebServer based on Undertow.
*
* @see <a href="http://undertow.io">Undertow</a>
*
* @author James Moger
*/
@MetaInfServices(WebServer.class)
public class UndertowServer extends AbstractWebServer<UndertowSettings> {
private static final Logger log = LoggerFactory.getLogger(UndertowServer.class);
private Undertow server;
private DeploymentManager pippoDeploymentManager;
@Override
public void start() {
try {
pippoDeploymentManager = createPippoDeploymentManager();
HttpHandler pippoHandler = pippoDeploymentManager.start();
HttpHandler contextHandler = createContextHandler(pippoHandler);
GracefulShutdownHandler rootHandler = new GracefulShutdownHandler(contextHandler);
server = createServer(rootHandler);
String version = server.getClass().getPackage().getImplementationVersion();
log.info("Starting Undertow Server {} on port {}", version, getSettings().getPort());
server.start();
} catch (Exception e) {
throw new PippoRuntimeException(e);
}
}
@Override
public void stop() {
if (server != null) {
try {
String version = server.getClass().getPackage().getImplementationVersion();
log.info("Stopping Undertow {} on port {}", version, getSettings().getPort());
server.stop();
pippoDeploymentManager.undeploy();
} catch (Exception e) {
throw new PippoRuntimeException(e, "Cannot stop Undertow Server");
}
}
}
@Override
protected UndertowSettings createDefaultSettings() {
return new UndertowSettings(getApplication().getPippoSettings());
}
protected Undertow createServer(HttpHandler contextHandler) {
Builder builder = Undertow.builder();
// TODO is it a better option?
if (getSettings().getBufferSize() > 0) {
builder.setBufferSize(getSettings().getBufferSize());
}
// method builder.setBuffersPerRegion is deprecated
/*
if (getSettings().getBuffersPerRegion() > 0) {
builder.setBuffersPerRegion(getSettings().getBuffersPerRegion());
}
*/
if (getSettings().getDirectBuffers()) {
builder.setDirectBuffers(getSettings().getDirectBuffers());
}
if (getSettings().getIoThreads() > 0) {
builder.setIoThreads(getSettings().getIoThreads());
}
if (getSettings().getWorkerThreads() > 0) {
builder.setWorkerThreads(getSettings().getWorkerThreads());
}
if (getSettings().getKeystoreFile() == null) {
// HTTP
builder.addHttpListener(getSettings().getPort(), getSettings().getHost());
} else {
// HTTPS
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true);
try {
KeyStore keyStore = loadKeyStore(getSettings().getKeystoreFile(), getSettings().getKeystorePassword());
KeyStore trustStore = loadKeyStore(getSettings().getTruststoreFile(), getSettings().getTruststorePassword());
SSLContext sslContext = createSSLContext(keyStore, trustStore);
builder.addHttpsListener(getSettings().getPort(), getSettings().getHost(), sslContext);
} catch (Exception e) {
throw new PippoRuntimeException(e, "Failed to setup an Undertow SSL listener!");
}
}
builder.setHandler(contextHandler);
return builder.build();
}
protected HttpHandler createContextHandler(HttpHandler pippoHandler) throws ServletException {
String contextPath = getSettings().getContextPath();
// create a handler than redirects non-contact requests to the context
PathHandler contextHandler = Handlers.path(Handlers.redirect(contextPath));
// add the handler with the context prefix
contextHandler.addPrefixPath(contextPath, pippoHandler);
return contextHandler;
}
@Override
protected PippoFilter createPippoFilter() {
return new UndertowWebSocketFilter();
}
protected DeploymentManager createPippoDeploymentManager() throws ServletException {
DeploymentInfo info = Servlets.deployment();
info.setDeploymentName("Pippo");
info.setClassLoader(this.getClass().getClassLoader());
info.setContextPath(getSettings().getContextPath());
info.setIgnoreFlush(true);
// inject application as context attribute
info.addServletContextAttribute(PIPPO_APPLICATION, getApplication());
// add pippo filter
addPippoFilter(info);
// add initializers
info.addListener(new ListenerInfo(PippoServletContextListener.class));
// add listeners
listeners.forEach(listener -> info.addListener(new ListenerInfo(listener)));
ServletInfo defaultServlet = new ServletInfo("DefaultServlet", DefaultServlet.class);
defaultServlet.addMapping("/");
MultipartConfigElement multipartConfig = createMultipartConfigElement();
defaultServlet.setMultipartConfig(multipartConfig);
info.addServlets(defaultServlet);
DeploymentManager deploymentManager = Servlets.defaultContainer().addDeployment(info);
deploymentManager.deploy();
return deploymentManager;
}
private MultipartConfigElement createMultipartConfigElement() {
Application application = getApplication();
String location = application.getUploadLocation();
long maxFileSize = application.getMaximumUploadSize();
return new MultipartConfigElement(location, maxFileSize, -1L, 0);
}
private void addPippoFilter(DeploymentInfo info) {
if (pippoFilterPath == null) {
pippoFilterPath = "/*"; // default value
}
info.addFilter(new FilterInfo("PippoFilter", PippoFilter.class, new ImmediateInstanceFactory<>(getPippoFilter())));
info.addFilterUrlMapping("PippoFilter", pippoFilterPath, DispatcherType.REQUEST);
log.debug("Using pippo filter for path '{}'", pippoFilterPath);
}
private SSLContext createSSLContext(final KeyStore keyStore, final KeyStore trustStore) throws Exception {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, getSettings().getKeystorePassword().toCharArray());
KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
SSLContext sslContext;
sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
return sslContext;
}
private KeyStore loadKeyStore(String filename, String password) throws Exception {
KeyStore loadedKeystore = KeyStore.getInstance("JKS");
File file = new File(filename);
if (file.exists()) {
try (InputStream stream = new FileInputStream(file)) {
loadedKeystore.load(stream, password.toCharArray());
}
} else {
log.error("Failed to find keystore '{}'!", filename);
}
return loadedKeystore;
}
}