/* * JBoss, Home of Professional Open Source. * Copyright 2011, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.domain.http.server; import static org.jboss.as.domain.http.server.logging.HttpServerLogger.ROOT_LOGGER; import static org.xnio.Options.SSL_CLIENT_AUTH_MODE; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLPeerUnverifiedException; import org.jboss.as.controller.ControlledProcessStateService; import org.jboss.as.controller.ModelController; import org.jboss.as.domain.http.server.cors.CorsHttpHandler; import org.jboss.as.domain.http.server.logging.HttpServerLogger; import org.jboss.as.domain.http.server.security.DmrFailureReadinessHandler; import org.jboss.as.domain.http.server.security.ElytronIdentityHandler; import org.jboss.as.domain.http.server.security.LogoutHandler; import org.jboss.as.domain.http.server.security.RedirectReadinessHandler; import org.jboss.as.domain.management.AuthMechanism; import org.jboss.as.domain.management.SecurityRealm; import org.jboss.modules.ModuleLoadException; import org.wildfly.common.Assert; import org.wildfly.elytron.web.undertow.server.ElytronContextAssociationHandler; import org.wildfly.elytron.web.undertow.server.ElytronHttpExchange; import org.wildfly.security.auth.server.HttpAuthenticationFactory; import org.wildfly.security.auth.server.SecurityIdentity; import org.wildfly.security.http.HttpServerAuthenticationMechanism; import org.xnio.BufferAllocator; import org.xnio.ByteBufferSlicePool; import org.xnio.ChannelListener; import org.xnio.ChannelListeners; import org.xnio.IoUtils; import org.xnio.OptionMap; import org.xnio.Options; import org.xnio.SslClientAuthMode; import org.xnio.StreamConnection; import org.xnio.XnioWorker; import org.xnio.channels.AcceptingChannel; import org.xnio.conduits.StreamSinkConduit; import org.xnio.ssl.SslConnection; import org.xnio.ssl.XnioSsl; import io.undertow.protocols.ssl.UndertowXnioSsl; import io.undertow.security.handlers.AuthenticationCallHandler; import io.undertow.security.handlers.AuthenticationConstraintHandler; import io.undertow.security.handlers.SinglePortConfidentialityHandler; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.server.RenegotiationRequiredException; import io.undertow.server.SSLSessionInfo; import io.undertow.server.handlers.BlockingHandler; import io.undertow.server.handlers.CanonicalPathHandler; import io.undertow.server.handlers.ChannelUpgradeHandler; import io.undertow.server.handlers.PathHandler; import io.undertow.server.handlers.ResponseCodeHandler; import io.undertow.server.handlers.SetHeaderHandler; import io.undertow.server.handlers.cache.CacheHandler; import io.undertow.server.handlers.cache.DirectBufferCache; import io.undertow.server.handlers.error.SimpleErrorPageHandler; import io.undertow.server.handlers.resource.ResourceManager; import io.undertow.server.protocol.http.HttpOpenListener; import io.undertow.util.Headers; import io.undertow.util.Methods; /** * The general HTTP server for handling management API requests. * * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> */ public class ManagementHttpServer { public interface PathRemapper { String remapPath(String originalPath); } private static final Map<Pattern, Charset> USER_AGENT_CHARSET_MAP = generateCharsetMap(); private static final Set<String> RESERVED_CONTEXTS; static { Set<String> set = new HashSet<>(); set.add(DomainApiCheckHandler.PATH); set.add(DomainApiCheckHandler.GENERIC_CONTENT_REQUEST); set.add(LogoutHandler.PATH); set.add(ErrorContextHandler.ERROR_CONTEXT); RESERVED_CONTEXTS = Collections.unmodifiableSet(set); } private final HttpOpenListener openListener; private final InetSocketAddress httpAddress; private final InetSocketAddress secureAddress; private final XnioWorker worker; private volatile AcceptingChannel<StreamConnection> normalServer; private volatile AcceptingChannel<SslConnection> secureServer; private final SSLContext sslContext; private final SslClientAuthMode sslClientAuthMode; private final ExtensionHandlers extensionHandlers; private ManagementHttpServer(HttpOpenListener openListener, InetSocketAddress httpAddress, InetSocketAddress secureAddress, SSLContext sslContext, SslClientAuthMode sslClientAuthMode, XnioWorker worker, ExtensionHandlers extensionExtensionHandlers) { this.openListener = openListener; this.httpAddress = httpAddress; this.secureAddress = secureAddress; this.sslContext = sslContext; this.sslClientAuthMode = sslClientAuthMode; this.worker = worker; this.extensionHandlers = extensionExtensionHandlers; } public void start() { try { OptionMap.Builder serverOptionsBuilder = OptionMap.builder() .set(Options.TCP_NODELAY, true) .set(Options.REUSE_ADDRESSES, true); ChannelListener acceptListener = ChannelListeners.openListenerAdapter(openListener); if (httpAddress != null) { normalServer = worker.createStreamConnectionServer(httpAddress, acceptListener, serverOptionsBuilder.getMap()); normalServer.resumeAccepts(); } if (secureAddress != null) { if (sslClientAuthMode != null) { serverOptionsBuilder.set(SSL_CLIENT_AUTH_MODE, sslClientAuthMode); } OptionMap secureOptions = serverOptionsBuilder.getMap(); XnioSsl xnioSsl = new UndertowXnioSsl(worker.getXnio(), secureOptions, sslContext); secureServer = xnioSsl.createSslConnectionServer(worker, secureAddress, acceptListener, secureOptions); secureServer.resumeAccepts(); } } catch (IOException e) { throw new RuntimeException(e); } } public void stop() { IoUtils.safeClose(normalServer); IoUtils.safeClose(secureServer); } public void addStaticContext(String contextName, ResourceManager resourceManager) { Assert.checkNotNullParam("contextName", contextName); Assert.checkNotNullParam("resourceManager", resourceManager); String context = fixPath(contextName); // Reject reserved contexts or duplicate extensions if (extensionHandlers.reservedContexts.contains(context) || !extensionHandlers.extensionContexts.add(context)) { throw new IllegalStateException(); } ResourceHandlerDefinition def = DomainUtil.createStaticContentHandler(resourceManager, context); HttpHandler readinessHandler = new RedirectReadinessHandler(extensionHandlers.readyFunction, def.getHandler(), ErrorContextHandler.ERROR_CONTEXT); extensionHandlers.extensionPathHandler.addPrefixPath(context, readinessHandler); } public void addManagementGetRemapContext(String contextName, PathRemapper remapper) { Assert.checkNotNullParam("contextName", contextName); String context = fixPath(contextName); // Reject reserved contexts or duplicate extensions if (extensionHandlers.reservedContexts.contains(context) || !extensionHandlers.extensionContexts.add(context)) { throw new IllegalStateException(); } HttpHandler remapHandler = new RemapHandler(remapper, extensionHandlers.managementHandler); extensionHandlers.extensionPathHandler.addPrefixPath(context, remapHandler); } public void removeContext(String contextName) { Assert.checkNotNullParam("contextName", contextName); String context = fixPath(contextName); // Reject reserved contexts or non-existent extensions if (extensionHandlers.reservedContexts.contains(context) || !extensionHandlers.extensionContexts.contains(context)) { throw new IllegalStateException(); } extensionHandlers.extensionContexts.remove(context); extensionHandlers.extensionPathHandler.removePrefixPath(context); } private static String fixPath(String contextName) { Assert.checkNotEmptyParam("contextName", contextName); return '/' == contextName.charAt(0) ? contextName : "/" + contextName; } private static SSLContext getSSLContext(Builder builder) { if (builder.sslContext != null) { return builder.sslContext; } else if (builder.securityRealm != null) { return builder.securityRealm.getSSLContext(); } else { throw ROOT_LOGGER.noRealmOrSSLContext(); } } private static ManagementHttpServer create(Builder builder) { SSLContext sslContext = null; SslClientAuthMode sslClientAuthMode = builder.sslClientAuthMode; if (builder.secureBindAddress != null) { sslContext = getSSLContext(builder); if (sslContext == null) { throw ROOT_LOGGER.sslRequestedNoSslContext(); } } HttpOpenListener openListener = new HttpOpenListener(new ByteBufferSlicePool(BufferAllocator.DIRECT_BYTE_BUFFER_ALLOCATOR, 4096, 10 * 4096)); int secureRedirectPort = builder.secureBindAddress != null ? builder.secureBindAddress.getPort() : -1; // WFLY-2870 -- redirect not supported if bindAddress and secureBindAddress are using different InetAddress boolean redirectSupported = (builder.bindAddress == null || builder.secureBindAddress == null || builder.bindAddress.getAddress().equals(builder.secureBindAddress.getAddress())); if (!redirectSupported && secureRedirectPort > 0) { HttpServerLogger.ROOT_LOGGER.httpsRedirectNotSupported(builder.bindAddress.getAddress(), builder.secureBindAddress.getAddress()); secureRedirectPort = -1; } final ExtensionHandlers extensionHandlers = setupOpenListener(openListener, secureRedirectPort, builder); return new ManagementHttpServer(openListener, builder.bindAddress, builder.secureBindAddress, sslContext, sslClientAuthMode, builder.worker, extensionHandlers); } private static Function<HttpServerExchange, Boolean> createReadyFunction(Builder builder) { if (builder.securityRealm != null) { final SecurityRealm securityRealm = builder.securityRealm; return e -> securityRealm.isReadyForHttpChallenge() || clientCertPotentiallyPossible(securityRealm, e); } else { return e -> Boolean.TRUE; } } private static boolean clientCertPotentiallyPossible(final SecurityRealm securityRealm, final HttpServerExchange exchange) { if (securityRealm.getSupportedAuthenticationMechanisms().contains(AuthMechanism.CLIENT_CERT) == false) { return false; } SSLSessionInfo session = exchange.getConnection().getSslSessionInfo(); if (session != null) { try { // todo: renegotiation? return session.getPeerCertificates()[0] instanceof X509Certificate; } catch (SSLPeerUnverifiedException | RenegotiationRequiredException e) { } } return false; } private static void addRedirectRedinessHandler(PathHandler pathHandler, ResourceHandlerDefinition consoleHandler, Function<HttpServerExchange, Boolean> readyFunction) { HttpHandler readinessHandler = new RedirectReadinessHandler(readyFunction, consoleHandler.getHandler(), ErrorContextHandler.ERROR_CONTEXT); pathHandler.addPrefixPath(consoleHandler.getContext(), readinessHandler); } private static HttpHandler addDmrRedinessHandler(PathHandler pathHandler, HttpHandler domainApiHandler, Function<HttpServerExchange, Boolean> readinessFunction) { HttpHandler readinessHandler = wrapXFrameOptions(new DmrFailureReadinessHandler(readinessFunction, domainApiHandler, ErrorContextHandler.ERROR_CONTEXT)); pathHandler.addPrefixPath(DomainApiCheckHandler.PATH, readinessHandler); pathHandler.addExactPath(DomainApiCheckHandler.GENERIC_CONTENT_REQUEST, readinessHandler); return readinessHandler; } private static void addLogoutHandler(PathHandler pathHandler, Builder builder) { if (builder.securityRealm != null) { pathHandler.addPrefixPath(LogoutHandler.PATH, wrapXFrameOptions(new LogoutHandler(builder.securityRealm.getName()))); } } private static class ExtensionHandlers { private final PathHandler extensionPathHandler; private final HttpHandler managementHandler; private final Function<HttpServerExchange, Boolean> readyFunction; private final Set<String> reservedContexts; private final Set<String> extensionContexts = new HashSet<>(); private ExtensionHandlers(PathHandler extensionPathHandler, HttpHandler managementHandler, Function<HttpServerExchange, Boolean> readyFunction, ResourceHandlerDefinition consoleHandler) { this.extensionPathHandler = extensionPathHandler; this.managementHandler = managementHandler; this.readyFunction = readyFunction; if (consoleHandler == null) { this.reservedContexts = RESERVED_CONTEXTS; } else { Set<String> set = new HashSet<>(RESERVED_CONTEXTS); set.add(consoleHandler.getContext()); this.reservedContexts = Collections.unmodifiableSet(set); } } } private static ExtensionHandlers setupOpenListener(HttpOpenListener listener, int secureRedirectPort, Builder builder) { CanonicalPathHandler canonicalPathHandler = new CanonicalPathHandler(); ManagementHttpRequestHandler managementHttpRequestHandler = new ManagementHttpRequestHandler(builder.managementHttpRequestProcessor, canonicalPathHandler); CorsHttpHandler corsHandler = new CorsHttpHandler(managementHttpRequestHandler, builder.allowedOrigins); listener.setRootHandler(new UpgradeFixHandler(corsHandler)); PathHandler pathHandler = new PathHandler(); HttpHandler current = pathHandler; if (builder.upgradeHandler != null) { builder.upgradeHandler.setNonUpgradeHandler(current); current = builder.upgradeHandler; } if (secureRedirectPort > 0) { // Add handler for redirect from http to https if needed current = new SinglePortConfidentialityHandler(current, secureRedirectPort); } // caching handler, used for static resources current = new CacheHandler(new DirectBufferCache(1024, 1024 * 10, 1024 * 1000, BufferAllocator.BYTE_BUFFER_ALLOCATOR), current); current = new SimpleErrorPageHandler(current); canonicalPathHandler.setNext(current); ResourceHandlerDefinition consoleHandler = null; try { consoleHandler = builder.consoleMode.createConsoleHandler(builder.consoleSlot); } catch (ModuleLoadException e) { ROOT_LOGGER.consoleModuleNotFound(builder.consoleSlot == null ? "main" : builder.consoleSlot); } try { pathHandler.addPrefixPath(ErrorContextHandler.ERROR_CONTEXT, ErrorContextHandler.createErrorContext(builder.consoleSlot)); } catch (ModuleLoadException e) { ROOT_LOGGER.errorContextModuleNotFound(builder.consoleSlot == null ? "main" : builder.consoleSlot); } ManagementRootConsoleRedirectHandler rootConsoleRedirectHandler = new ManagementRootConsoleRedirectHandler(consoleHandler); HttpHandler domainApiHandler = InExecutorHandler.wrap( builder.executor, associateIdentity(new DomainApiCheckHandler(builder.modelController, builder.controlledProcessStateService, builder.allowedOrigins), builder) ); final Function<HttpServerExchange, Boolean> readyFunction = createReadyFunction(builder); pathHandler.addPrefixPath("/", rootConsoleRedirectHandler); if (consoleHandler != null) { addRedirectRedinessHandler(pathHandler, consoleHandler, readyFunction); } domainApiHandler = secureDomainAccess(domainApiHandler, builder); HttpHandler readinessHandler = addDmrRedinessHandler(pathHandler, domainApiHandler, readyFunction); addLogoutHandler(pathHandler, builder); return new ExtensionHandlers(pathHandler, readinessHandler, readyFunction, consoleHandler); } private static HttpHandler associateIdentity(HttpHandler domainHandler, final Builder builder) { domainHandler = new ElytronIdentityHandler(domainHandler); return new BlockingHandler(domainHandler); } private static HttpHandler secureDomainAccess(HttpHandler domainHandler, final Builder builder) { if (builder.httpAuthenticationFactory != null) { return secureDomainAccess(domainHandler, builder.httpAuthenticationFactory); } else if (builder.securityRealm != null) { HttpAuthenticationFactory httpAuthenticationFactory = builder.securityRealm.getHttpAuthenticationFactory(); if (httpAuthenticationFactory != null) { return secureDomainAccess(domainHandler, httpAuthenticationFactory); } } return domainHandler; } private static Map<Pattern, Charset> generateCharsetMap() { final Map<Pattern, Charset> charsetMap = new HashMap<>(); charsetMap.put(Pattern.compile("Mozilla/5\\.0 \\(.*\\) Gecko/.* Firefox/.*"), StandardCharsets.ISO_8859_1); charsetMap.put(Pattern.compile("(?!.*OPR)(?!.*Chrome)Mozilla/5\\.0 \\(.*\\).* Safari/.*"), StandardCharsets.ISO_8859_1); charsetMap.put(Pattern.compile("Mozilla/5\\.0 \\(.*; Trident/.*; rv:.*\\).*"), StandardCharsets.ISO_8859_1); charsetMap.put(Pattern.compile("Mozilla/5\\.0 \\(.* MSIE.* Trident/.*\\)"), StandardCharsets.ISO_8859_1); return Collections.unmodifiableMap(charsetMap); } private static HttpHandler secureDomainAccess(HttpHandler domainHandler, final HttpAuthenticationFactory httpAuthenticationFactory) { domainHandler = new AuthenticationCallHandler(domainHandler); domainHandler = new AuthenticationConstraintHandler(domainHandler); Supplier<List<HttpServerAuthenticationMechanism>> mechanismSupplier = () -> httpAuthenticationFactory.getMechanismNames().stream() .map(s -> { try { return httpAuthenticationFactory.createMechanism(s); } catch (Exception e) { return null; } }) .collect(Collectors.toList()); domainHandler = ElytronContextAssociationHandler.builder() .setNext(domainHandler) .setMechanismSupplier(mechanismSupplier) .setHttpExchangeSupplier(h -> new ElytronHttpExchange(h) { @Override public void authenticationComplete(SecurityIdentity securityIdentity, String mechanismName) { super.authenticationComplete(securityIdentity, mechanismName); h.putAttachment(ElytronIdentityHandler.IDENTITY_KEY, securityIdentity); } }) .build(); return domainHandler; } private static HttpHandler wrapXFrameOptions(final HttpHandler toWrap) { return new SetHeaderHandler(toWrap, "X-Frame-Options", "SAMEORIGIN"); } public static Builder builder() { return new Builder(); } public static class Builder { private boolean built = false; private InetSocketAddress bindAddress; private InetSocketAddress secureBindAddress; private ModelController modelController; private SecurityRealm securityRealm; private SSLContext sslContext; private SslClientAuthMode sslClientAuthMode; private HttpAuthenticationFactory httpAuthenticationFactory; private ControlledProcessStateService controlledProcessStateService; private ConsoleMode consoleMode; private String consoleSlot; private ChannelUpgradeHandler upgradeHandler; private ManagementHttpRequestProcessor managementHttpRequestProcessor; private Collection<String> allowedOrigins; private XnioWorker worker; private Executor executor; private Builder() { } public Builder setBindAddress(InetSocketAddress bindAddress) { assertNotBuilt(); this.bindAddress = bindAddress; return this; } public Builder setSecureBindAddress(InetSocketAddress secureBindAddress) { assertNotBuilt(); this.secureBindAddress = secureBindAddress; return this; } public Builder setModelController(ModelController modelController) { assertNotBuilt(); this.modelController = modelController; return this; } public Builder setSecurityRealm(SecurityRealm securityRealm) { assertNotBuilt(); this.securityRealm = securityRealm; return this; } public Builder setSSLContext(SSLContext sslContext) { assertNotBuilt(); this.sslContext = sslContext; return this; } /** * Set the SSL client authentication mode. * * Note: This should only be used for {@link SecurityRealm} provided {@link SSLContext} instances. * * @param sslClientAuthMode the SSL client authentication mode. * @return {@code this} to allow chaining of commands. */ public Builder setSSLClientAuthMode(SslClientAuthMode sslClientAuthMode) { assertNotBuilt(); this.sslClientAuthMode = sslClientAuthMode; return this; } public Builder setHttpAuthenticationFactory(HttpAuthenticationFactory httpAuthenticationFactory) { assertNotBuilt(); this.httpAuthenticationFactory = httpAuthenticationFactory; return this; } public Builder setControlledProcessStateService(ControlledProcessStateService controlledProcessStateService) { assertNotBuilt(); this.controlledProcessStateService = controlledProcessStateService; return this; } public Builder setConsoleMode(ConsoleMode consoleMode) { assertNotBuilt(); this.consoleMode = consoleMode; return this; } public Builder setConsoleSlot(String consoleSlot) { assertNotBuilt(); this.consoleSlot = consoleSlot; return this; } public Builder setChannelUpgradeHandler(ChannelUpgradeHandler upgradeHandler) { assertNotBuilt(); this.upgradeHandler = upgradeHandler; return this; } public Builder setManagementHttpRequestProcessor(ManagementHttpRequestProcessor managementHttpRequestProcessor) { assertNotBuilt(); this.managementHttpRequestProcessor = managementHttpRequestProcessor; return this; } public Builder setAllowedOrigins(Collection<String> allowedOrigins) { assertNotBuilt(); this.allowedOrigins = allowedOrigins; return this; } public Builder setWorker(XnioWorker worker) { assertNotBuilt(); this.worker = worker; return this; } public Builder setExecutor(Executor executor) { assertNotBuilt(); this.executor = executor; return this; } public ManagementHttpServer build() { assertNotBuilt(); ManagementHttpServer managementHttpServer = create(this); built = true; return managementHttpServer; } private void assertNotBuilt() { if (built) { throw ROOT_LOGGER.managementHttpServerAlreadyBuild(); } } } /** * Handler to work around a bug with old XNIO versions that did not handle * content-length for HTTP upgrade. This should be removed when it is no longer * nessesary to support WF 8.x clients. */ private static class UpgradeFixHandler implements HttpHandler { final HttpHandler next; private UpgradeFixHandler(HttpHandler next) { this.next = next; } @Override public void handleRequest(HttpServerExchange exchange) throws Exception { if(exchange.getRequestHeaders().contains(Headers.UPGRADE)) { exchange.addResponseWrapper((factory, ex) -> { StreamSinkConduit ret = factory.create(); if(exchange.getResponseHeaders().contains(Headers.UPGRADE)) { exchange.getResponseHeaders().add(Headers.CONTENT_LENGTH, "0"); } return ret; }); } next.handleRequest(exchange); } } private static class RemapHandler implements HttpHandler { private final PathRemapper remapper; private final HttpHandler next; private RemapHandler(PathRemapper remapper, HttpHandler next) { this.remapper = remapper; this.next = next; } @Override public void handleRequest(HttpServerExchange exchange) throws Exception { if (Methods.POST.equals(exchange.getRequestMethod())) { ResponseCodeHandler.HANDLE_405.handleRequest(exchange); return; } String origReqPath = exchange.getRelativePath(); String remapped = remapper.remapPath(origReqPath); if (remapped == null) { ResponseCodeHandler.HANDLE_404.handleRequest(exchange); return; } exchange.setRelativePath(remapped); // Note: we only change the relative path, not other exchange data that // incorporates it (like getRequestPath(), getRequestURL()) and not the // resolved path. If this request gets to DomainApiHandler, it should // work off the relative path. Other handlers in between may need the // original data. next.handleRequest(exchange); } } }