/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.cxf.transport.http_undertow; import java.io.IOException; import java.net.URL; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.X509KeyManager; import javax.servlet.ServletContext; import javax.servlet.ServletException; import org.apache.cxf.Bus; import org.apache.cxf.common.i18n.Message; import org.apache.cxf.common.logging.LogUtils; import org.apache.cxf.common.util.PropertyUtils; import org.apache.cxf.common.util.SystemPropertyAction; import org.apache.cxf.configuration.jsse.TLSServerParameters; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.transport.HttpUriMapper; import org.apache.cxf.transport.https.AliasedX509ExtendedKeyManager; import org.xnio.Options; import org.xnio.Sequence; import org.xnio.SslClientAuthMode; 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.PathHandler; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.DeploymentManager; import io.undertow.servlet.api.ServletContainer; import io.undertow.servlet.api.ServletInfo; import io.undertow.servlet.core.ServletContainerImpl; import io.undertow.servlet.handlers.ServletPathMatches; import io.undertow.util.CopyOnWriteMap; public class UndertowHTTPServerEngine implements ServerEngine { public static final String DO_NOT_CHECK_URL_PROP = "org.apache.cxf.transports.http_undertow.DontCheckUrl"; private static final Logger LOG = LogUtils.getL7dLogger(UndertowHTTPServerEngine.class); /** * This is the network port for which this engine is allocated. */ private int port; /** * This is the network address for which this engine is allocated. */ private String host; /** * This field holds the protocol for which this engine is * enabled, i.e. "http" or "https". */ private String protocol = "http"; private int servantCount; private Undertow server; /** * This field holds the TLS ServerParameters that are programatically * configured. The tlsServerParamers (due to JAXB) holds the struct * placed by SpringConfig. */ private TLSServerParameters tlsServerParameters; private SSLContext sslContext; /** * This boolean signfies that SpringConfig is over. finalizeConfig * has been called. */ private boolean configFinalized; private ConcurrentMap<String, UndertowHTTPHandler> registedPaths = new CopyOnWriteMap<String, UndertowHTTPHandler>(); private boolean continuationsEnabled = true; private ServletContext servletContext; private PathHandler path; private int maxIdleTime = 200000; private org.apache.cxf.transport.http_undertow.ThreadingParameters threadingParameters; private List<CXFUndertowHttpHandler> handlers; public UndertowHTTPServerEngine(String host, int port) { this.host = host; this.port = port; } public UndertowHTTPServerEngine() { } @Override public void addServant(URL url, UndertowHTTPHandler handler) { if (shouldCheckUrl(handler.getBus())) { checkRegistedContext(url); } if (server == null) { try { // create a new undertow server instance if there is no server there String contextName = HttpUriMapper.getContextName(url.getPath()); servletContext = buildServletContext(contextName); handler.setServletContext(servletContext); server = createServer(url, handler); server.start(); } catch (Exception e) { LOG.log(Level.SEVERE, "START_UP_SERVER_FAILED_MSG", new Object[] {e.getMessage(), port}); //problem starting server try { server.stop(); } catch (Exception ex) { //ignore - probably wasn't fully started anyway } server = null; throw new Fault(new Message("START_UP_SERVER_FAILED_MSG", LOG, e.getMessage(), port), e); } } else { String contextName = HttpUriMapper.getContextName(url.getPath()); try { servletContext = buildServletContext(contextName); } catch (ServletException e) { throw new Fault(new Message("START_UP_SERVER_FAILED_MSG", LOG, e.getMessage(), port), e); } handler.setServletContext(servletContext); if (handler.isContextMatchExact()) { path.addExactPath(url.getPath(), handler); } else { path.addPrefixPath(url.getPath(), handler); } } final String smap = HttpUriMapper.getResourceBase(url.getPath()); handler.setName(smap); registedPaths.put(url.getPath(), handler); servantCount = servantCount + 1; } private ServletContext buildServletContext(String contextName) throws ServletException { ServletContainer servletContainer = new ServletContainerImpl(); DeploymentInfo deploymentInfo = new DeploymentInfo(); deploymentInfo.setClassLoader(Thread.currentThread().getContextClassLoader()); deploymentInfo.setDeploymentName("cxf-undertow"); deploymentInfo.setContextPath(contextName); ServletInfo asyncServlet = new ServletInfo(ServletPathMatches.DEFAULT_SERVLET_NAME, CxfUndertowServlet.class); deploymentInfo.addServlet(asyncServlet); servletContainer.addDeployment(deploymentInfo); DeploymentManager deploymentManager = servletContainer.getDeployment(deploymentInfo.getDeploymentName()); deploymentManager.deploy(); deploymentManager.start(); return deploymentManager.getDeployment().getServletContext(); } private Undertow createServer(URL url, UndertowHTTPHandler undertowHTTPHandler) throws Exception { Undertow.Builder result = Undertow.builder(); result.setServerOption(UndertowOptions.IDLE_TIMEOUT, getMaxIdleTime()); if (tlsServerParameters != null) { if (this.sslContext == null) { this.sslContext = createSSLContext(); } result = result.addHttpsListener(getPort(), getHost(), this.sslContext); } else { result = result.addHttpListener(getPort(), getHost()); } path = Handlers.path(new NotFoundHandler()); if (url.getPath().length() == 0) { result = result.setHandler(Handlers.trace(undertowHTTPHandler)); } else { if (undertowHTTPHandler.isContextMatchExact()) { path.addExactPath(url.getPath(), undertowHTTPHandler); } else { path.addPrefixPath(url.getPath(), undertowHTTPHandler); } result = result.setHandler(wrapHandler(path)); } result = decorateUndertowSocketConnection(result); result = disableSSLv3(result); result = configureThreads(result); return result.build(); } private Builder configureThreads(Builder builder) { if (this.threadingParameters != null) { if (this.threadingParameters.isWorkerIOThreadsSet()) { builder = builder.setWorkerOption(Options.WORKER_IO_THREADS, this.threadingParameters.getWorkerIOThreads()); } if (this.threadingParameters.isMinThreadsSet()) { builder = builder.setWorkerOption(Options.WORKER_TASK_CORE_THREADS, this.threadingParameters.getMinThreads()); } if (this.threadingParameters.isMaxThreadsSet()) { builder = builder.setWorkerOption(Options.WORKER_TASK_MAX_THREADS, this.threadingParameters.getMaxThreads()); } } return builder; } private HttpHandler wrapHandler(HttpHandler handler) { HttpHandler nextHandler = handler; for (CXFUndertowHttpHandler h : getHandlers()) { h.setNext(nextHandler); nextHandler = h; } return nextHandler; } private Builder disableSSLv3(Builder result) { //SSLv3 isn't safe, disable it by default unless explicitly use it if (tlsServerParameters != null && ("SSLv3".equals(tlsServerParameters.getSecureSocketProtocol()) || !tlsServerParameters.getIncludeProtocols().isEmpty())) { List<String> protocols = new LinkedList<String>(Arrays.asList("TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3")); for (String excludedProtocol : tlsServerParameters.getExcludeProtocols()) { if (protocols.contains(excludedProtocol)) { protocols.remove(excludedProtocol); } } Sequence<String> supportProtocols = Sequence.of(protocols); return result.setSocketOption(Options.SSL_ENABLED_PROTOCOLS, supportProtocols); } else { Sequence<String> supportProtocols = Sequence.of("TLSv1", "TLSv1.1", "TLSv1.2"); return result.setSocketOption(Options.SSL_ENABLED_PROTOCOLS, supportProtocols); } } public Undertow.Builder decorateUndertowSocketConnection(Undertow.Builder builder) { if (this.tlsServerParameters != null && this.tlsServerParameters.getClientAuthentication() != null && this.tlsServerParameters.getClientAuthentication().isRequired()) { builder = builder.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, SslClientAuthMode.REQUIRED); } if (this.tlsServerParameters != null && this.tlsServerParameters.getClientAuthentication() != null && this.tlsServerParameters.getClientAuthentication().isWant()) { builder = builder.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, SslClientAuthMode.REQUESTED); } return builder; } private boolean shouldCheckUrl(Bus bus) { Object prop = null; if (bus != null) { prop = bus.getProperty(DO_NOT_CHECK_URL_PROP); } if (prop == null) { prop = SystemPropertyAction.getPropertyOrNull(DO_NOT_CHECK_URL_PROP); } return !PropertyUtils.isTrue(prop); } protected void checkRegistedContext(URL url) { String urlPath = url.getPath(); for (String registedPath : registedPaths.keySet()) { if (urlPath.equals(registedPath)) { throw new Fault(new Message("ADD_HANDLER_CONTEXT_IS_USED_MSG", LOG, url, registedPath)); } // There are some context path conflicts which could cause the UndertowHTTPServerEngine // doesn't route the message to the right UndertowHTTPHandler if (urlPath.equals(HttpUriMapper.getContextName(registedPath))) { throw new Fault(new Message("ADD_HANDLER_CONTEXT_IS_USED_MSG", LOG, url, registedPath)); } if (registedPath.equals(HttpUriMapper.getContextName(urlPath))) { throw new Fault(new Message("ADD_HANDLER_CONTEXT_CONFILICT_MSG", LOG, url, registedPath)); } } } @Override public void removeServant(URL url) { UndertowHTTPHandler handler = registedPaths.remove(url.getPath()); if (handler == null) { return; } --servantCount; if (url.getPath().isEmpty()) { return; } if (handler.isContextMatchExact()) { path.removeExactPath(url.getPath()); } else { path.removePrefixPath(url.getPath()); } } @Override public UndertowHTTPHandler getServant(URL url) { return registedPaths.get(url.getPath()); } /** * Returns the protocol "http" or "https" for which this engine * was configured. */ public String getProtocol() { return protocol; } /** * Returns the port number for which this server engine was configured. * @return */ public int getPort() { return port; } /** * Returns the host for which this server engine was configured. * @return */ public String getHost() { return host; } public void setPort(int p) { port = p; } public void setHost(String host) { this.host = host; } public void finalizeConfig() throws GeneralSecurityException, IOException { retrieveListenerFactory(); this.configFinalized = true; } /** * This method is used to programmatically set the TLSServerParameters. * This method may only be called by the factory. * @throws IOException */ public void setTlsServerParameters(TLSServerParameters params) { tlsServerParameters = params; if (this.configFinalized) { this.retrieveListenerFactory(); } } private void retrieveListenerFactory() { if (tlsServerParameters != null) { protocol = "https"; } else { protocol = "http"; } LOG.fine("Configured port " + port + " for \"" + protocol + "\"."); } /** * This method returns the programmatically set TLSServerParameters, not * the TLSServerParametersType, which is the JAXB generated type used * in SpringConfiguration. * @return */ public TLSServerParameters getTlsServerParameters() { return tlsServerParameters; } public void stop() { if (this.server != null) { this.server.stop(); } } /** * This method will shut down the server engine and * remove it from the factory's cache. */ public void shutdown() { registedPaths.clear(); if (shouldDestroyPort()) { if (servantCount == 0) { UndertowHTTPServerEngineFactory.destroyForPort(port); } else { LOG.log(Level.WARNING, "FAILED_TO_SHUTDOWN_ENGINE_MSG", port); } } } private boolean shouldDestroyPort() { //if we shutdown the port, on SOME OS's/JVM's, if a client //in the same jvm had been talking to it at some point and keep alives //are on, then the port is held open for about 60 seconds //afterwards and if we restart, connections will then //get sent into the old stuff where there are //no longer any servant registered. They pretty much just hang. //this is most often seen in our unit/system tests that //test things in the same VM. String s = SystemPropertyAction .getPropertyOrNull("org.apache.cxf.transports.http_undertow.DontClosePort." + port); if (s == null) { s = SystemPropertyAction .getPropertyOrNull("org.apache.cxf.transports.http_undertow.DontClosePort"); } return !Boolean.valueOf(s); } protected SSLContext createSSLContext() throws Exception { String proto = tlsServerParameters.getSecureSocketProtocol() == null ? "TLS" : tlsServerParameters.getSecureSocketProtocol(); SSLContext context = tlsServerParameters.getJsseProvider() == null ? SSLContext.getInstance(proto) : SSLContext.getInstance(proto, tlsServerParameters.getJsseProvider()); KeyManager[] keyManagers = tlsServerParameters.getKeyManagers(); if (tlsServerParameters.getCertAlias() != null) { keyManagers = getKeyManagersWithCertAlias(keyManagers); } context.init(keyManagers, tlsServerParameters.getTrustManagers(), tlsServerParameters.getSecureRandom()); return context; } protected KeyManager[] getKeyManagersWithCertAlias(KeyManager keyManagers[]) throws Exception { if (tlsServerParameters.getCertAlias() != null) { for (int idx = 0; idx < keyManagers.length; idx++) { if (keyManagers[idx] instanceof X509KeyManager) { keyManagers[idx] = new AliasedX509ExtendedKeyManager( tlsServerParameters.getCertAlias(), (X509KeyManager)keyManagers[idx]); } } } return keyManagers; } /** * This method sets the threading parameters for this particular * server engine. * This method may only be called by the factory. */ public void setThreadingParameters(ThreadingParameters params) { threadingParameters = params; } /** * This method returns whether the threading parameters are set. */ public boolean isSetThreadingParameters() { return threadingParameters != null; } /** * This method returns the threading parameters that have been set. * This method may return null, if the threading parameters have not * been set. */ public ThreadingParameters getThreadingParameters() { return threadingParameters; } public void setContinuationsEnabled(boolean enabled) { continuationsEnabled = enabled; } public boolean getContinuationsEnabled() { return continuationsEnabled; } public int getMaxIdleTime() { return maxIdleTime; } public void setMaxIdleTime(int maxIdleTime) { this.maxIdleTime = maxIdleTime; } /** * set the Undertow server's handlers * @param h */ public void setHandlers(List<CXFUndertowHttpHandler> h) { handlers = h; } public List<CXFUndertowHttpHandler> getHandlers() { return handlers != null ? handlers : new ArrayList<>(); } }