/* * JBoss, Home of Professional Open Source. * Copyright 2017, Red Hat Middleware LLC, 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.wildfly.extension.mod_cluster; import static org.jboss.as.clustering.dmr.ModelNodes.optionalList; import static org.jboss.as.clustering.dmr.ModelNodes.optionalString; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.ADVERTISE; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.ADVERTISE_SECURITY_KEY; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.ADVERTISE_SOCKET; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.AUTO_ENABLE_CONTEXTS; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.BALANCER; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.EXCLUDED_CONTEXTS; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.FLUSH_PACKETS; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.FLUSH_WAIT; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.LOAD_BALANCING_GROUP; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.MAX_ATTEMPTS; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.NODE_TIMEOUT; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.PING; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.PROXIES; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.PROXY_URL; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.SESSION_DRAINING_STRATEGY; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.SMAX; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.SOCKET_TIMEOUT; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.SSL_CONTEXT; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.STICKY_SESSION; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.STICKY_SESSION_FORCE; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.STICKY_SESSION_REMOVE; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.STOP_CONTEXT_TIMEOUT; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.TTL; import static org.wildfly.extension.mod_cluster.ModClusterConfigResourceDefinition.WORKER_TIMEOUT; import static org.wildfly.extension.mod_cluster.ModClusterLogger.ROOT_LOGGER; import static org.wildfly.extension.mod_cluster.ModClusterSSLResourceDefinition.CA_CERTIFICATE_FILE; import static org.wildfly.extension.mod_cluster.ModClusterSSLResourceDefinition.CA_REVOCATION_URL; import static org.wildfly.extension.mod_cluster.ModClusterSSLResourceDefinition.CERTIFICATE_KEY_FILE; import static org.wildfly.extension.mod_cluster.ModClusterSSLResourceDefinition.CIPHER_SUITE; import static org.wildfly.extension.mod_cluster.ModClusterSSLResourceDefinition.KEY_ALIAS; import static org.wildfly.extension.mod_cluster.ModClusterSSLResourceDefinition.PASSWORD; import static org.wildfly.extension.mod_cluster.ModClusterSSLResourceDefinition.PROTOCOL; import java.net.InetSocketAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import javax.net.ssl.SSLContext; import org.jboss.as.clustering.controller.CommonUnaryRequirement; import org.jboss.as.clustering.controller.ResourceServiceBuilder; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.network.OutboundSocketBinding; import org.jboss.as.network.SocketBinding; import org.jboss.dmr.ModelNode; import org.jboss.modcluster.config.ModClusterConfiguration; import org.jboss.modcluster.config.ProxyConfiguration; import org.jboss.modcluster.config.builder.ModClusterConfigurationBuilder; import org.jboss.modcluster.config.impl.ModClusterConfig; import org.jboss.modcluster.config.impl.SessionDrainingStrategyEnum; import org.jboss.modcluster.mcmp.impl.JSSESocketFactory; import org.jboss.msc.service.ServiceBuilder; import org.jboss.msc.service.ServiceController; import org.jboss.msc.service.ServiceName; import org.jboss.msc.service.ServiceTarget; import org.jboss.msc.service.ValueService; import org.jboss.msc.value.Value; import org.wildfly.clustering.service.Builder; import org.wildfly.clustering.service.InjectedValueDependency; import org.wildfly.clustering.service.ValueDependency; /** * @author Radoslav Husar */ public class ModClusterConfigurationServiceBuilder implements ResourceServiceBuilder<ModClusterConfiguration>, Value<ModClusterConfiguration> { private final ModClusterConfigurationBuilder builder = new ModClusterConfigurationBuilder(); private ValueDependency<SocketBinding> advertiseSocketDependency = null; private final List<ValueDependency<OutboundSocketBinding>> outboundSocketBindings = new LinkedList<>(); private ValueDependency<SSLContext> sslContextDependency = null; @Override public ServiceName getServiceName() { return ContainerEventHandlerService.CONFIG_SERVICE_NAME; } @Override public Builder<ModClusterConfiguration> configure(OperationContext context, ModelNode model) throws OperationFailedException { // Advertise optionalString(ADVERTISE_SOCKET.resolveModelAttribute(context, model)) .ifPresent(advertiseSocketRef -> this.advertiseSocketDependency = new InjectedValueDependency<>(context.getCapabilityServiceName(CommonUnaryRequirement.SOCKET_BINDING.getName(), advertiseSocketRef, SocketBinding.class), SocketBinding.class)); optionalString(ADVERTISE_SECURITY_KEY.resolveModelAttribute(context, model)) .ifPresent(securityKey -> builder.advertise().setAdvertiseSecurityKey(securityKey)); // MCMP builder.mcmp() .setAdvertise(ADVERTISE.resolveModelAttribute(context, model).asBoolean()) .setProxyURL(PROXY_URL.resolveModelAttribute(context, model).asString()) .setAutoEnableContexts(AUTO_ENABLE_CONTEXTS.resolveModelAttribute(context, model).asBoolean()) .setStopContextTimeout(STOP_CONTEXT_TIMEOUT.resolveModelAttribute(context, model).asInt()) .setStopContextTimeoutUnit(TimeUnit.valueOf(STOP_CONTEXT_TIMEOUT.getMeasurementUnit().getName())) .setSocketTimeout(SOCKET_TIMEOUT.resolveModelAttribute(context, model).asInt() * 1000) .setSessionDrainingStrategy(Enum.valueOf(SessionDrainingStrategyEnum.class, SESSION_DRAINING_STRATEGY.resolveModelAttribute(context, model).asString())) ; if (model.hasDefined(CommonAttributes.EXCLUDED_CONTEXTS)) { String contexts = EXCLUDED_CONTEXTS.resolveModelAttribute(context, model).asString(); Map<String, Set<String>> excludedContextsPerHost; if (contexts == null) { excludedContextsPerHost = Collections.emptyMap(); } else { String trimmedContexts = contexts.trim(); if (trimmedContexts.isEmpty()) { excludedContextsPerHost = Collections.emptyMap(); } else { excludedContextsPerHost = new HashMap<>(); for (String c : trimmedContexts.split(",")) { String[] parts = c.trim().split(":"); if (parts.length > 2) { throw ROOT_LOGGER.excludedContextsWrongFormat(trimmedContexts); } String host = null; String trimmedContext = parts[0].trim(); if (parts.length == 2) { host = trimmedContext; trimmedContext = parts[1].trim(); } String path = trimmedContext.equals("ROOT") ? "" : "/" + trimmedContext; Set<String> paths = excludedContextsPerHost.computeIfAbsent(host, k -> new HashSet<>()); paths.add(path); } } } builder.mcmp().setExcludedContextsPerHost(excludedContextsPerHost); } // Balancer builder.balancer() .setStickySession(STICKY_SESSION.resolveModelAttribute(context, model).asBoolean()) .setStickySessionRemove(STICKY_SESSION_REMOVE.resolveModelAttribute(context, model).asBoolean()) .setStickySessionForce(STICKY_SESSION_FORCE.resolveModelAttribute(context, model).asBoolean()) .setWorkerTimeout(WORKER_TIMEOUT.resolveModelAttribute(context, model).asInt()) .setMaxAttempts(MAX_ATTEMPTS.resolveModelAttribute(context, model).asInt()) ; // Node builder.node() .setFlushPackets(FLUSH_PACKETS.resolveModelAttribute(context, model).asBoolean()) .setFlushWait(FLUSH_WAIT.resolveModelAttribute(context, model).asInt()) .setPing(PING.resolveModelAttribute(context, model).asInt()) .setSmax(SMAX.resolveModelAttribute(context, model).asInt()) .setTtl(TTL.resolveModelAttribute(context, model).asInt()) .setNodeTimeout(NODE_TIMEOUT.resolveModelAttribute(context, model).asInt()) ; optionalString(BALANCER.resolveModelAttribute(context, model)).ifPresent(balancer -> builder.node().setBalancer(balancer)); optionalString(LOAD_BALANCING_GROUP.resolveModelAttribute(context, model)).ifPresent(group -> builder.node().setLoadBalancingGroup(group)); optionalList(PROXIES.resolveModelAttribute(context, model)).ifPresent( refs -> refs.stream() .map(ModelNode::asString) .forEach(ref -> this.outboundSocketBindings.add(new InjectedValueDependency<>(CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING.getServiceName(context, ref), OutboundSocketBinding.class))) ); if (model.hasDefined(CommonAttributes.PROXY_LIST)) { throw new OperationFailedException(ROOT_LOGGER.proxyListNotAllowedInCurrentModel()); } // Elytron-based security support Optional<String> sslContextRef = optionalString(SSL_CONTEXT.resolveModelAttribute(context, model)); sslContextRef.ifPresent( ref -> this.sslContextDependency = new InjectedValueDependency<>(CommonUnaryRequirement.SSL_CONTEXT.getServiceName(context, ref), SSLContext.class) ); // Legacy security support if (model.get(ModClusterSSLResourceDefinition.PATH.getKeyValuePair()).isDefined()) { if (sslContextRef.isPresent()) { throw ROOT_LOGGER.bothElytronAndLegacySslContextDefined(); } ModelNode sslModel = model.get(ModClusterSSLResourceDefinition.PATH.getKeyValuePair()); ModClusterConfig sslConfiguration = new ModClusterConfig(); optionalString(KEY_ALIAS.resolveModelAttribute(context, sslModel)).ifPresent(sslConfiguration::setSslKeyAlias); optionalString(PASSWORD.resolveModelAttribute(context, sslModel)).ifPresent(sslConfiguration::setSslTrustStorePassword); optionalString(PASSWORD.resolveModelAttribute(context, sslModel)).ifPresent(sslConfiguration::setSslKeyStorePassword); optionalString(CERTIFICATE_KEY_FILE.resolveModelAttribute(context, sslModel)).ifPresent(sslConfiguration::setSslKeyStore); optionalString(CIPHER_SUITE.resolveModelAttribute(context, sslModel)).ifPresent(sslConfiguration::setSslCiphers); optionalString(PROTOCOL.resolveModelAttribute(context, sslModel)).ifPresent(sslConfiguration::setSslProtocol); optionalString(CA_CERTIFICATE_FILE.resolveModelAttribute(context, sslModel)).ifPresent(sslConfiguration::setSslTrustStore); optionalString(CA_REVOCATION_URL.resolveModelAttribute(context, sslModel)).ifPresent(sslConfiguration::setSslCrlFile); builder.mcmp().setSocketFactory(new JSSESocketFactory(sslConfiguration)); } return this; } @Override public ServiceBuilder<ModClusterConfiguration> build(ServiceTarget target) { ServiceBuilder<ModClusterConfiguration> builder = target.addService(this.getServiceName(), new ValueService<>(this)); Stream.concat(Stream.of(advertiseSocketDependency, sslContextDependency), outboundSocketBindings.stream()).filter(Objects::nonNull).forEach(dependency -> dependency.register(builder)); builder.setInitialMode(ServiceController.Mode.PASSIVE); return builder; } @Override public ModClusterConfiguration getValue() throws IllegalStateException, IllegalArgumentException { // Advertise if (advertiseSocketDependency != null) { final SocketBinding binding = advertiseSocketDependency.getValue(); builder.advertise() .setAdvertiseSocketAddress(binding.getMulticastSocketAddress()) .setAdvertiseInterface(binding.getSocketAddress().getAddress()) ; if (!isMulticastEnabled(binding.getSocketBindings().getDefaultInterfaceBinding().getNetworkInterfaces())) { ROOT_LOGGER.multicastInterfaceNotAvailable(); } } // Proxies List<ProxyConfiguration> proxies = new LinkedList<>(); for (final ValueDependency<OutboundSocketBinding> outboundSocketBindingValueDependency : outboundSocketBindings) { OutboundSocketBinding binding = outboundSocketBindingValueDependency.getValue(); proxies.add(new ProxyConfiguration() { @Override public InetSocketAddress getRemoteAddress() { // Both host and port may not be null in the model, no need to validate here // Don't do resolving here, let mod_cluster deal with it return new InetSocketAddress(binding.getUnresolvedDestinationAddress(), binding.getDestinationPort()); } @Override public InetSocketAddress getLocalAddress() { if (binding.getOptionalSourceAddress() != null) { return new InetSocketAddress(binding.getOptionalSourceAddress(), binding.getAbsoluteSourcePort() == null ? 0 : binding.getAbsoluteSourcePort()); } else if (binding.getAbsoluteSourcePort() != null) { // Bind to port only if source address is not configured return new InetSocketAddress(binding.getAbsoluteSourcePort()); } // No binding configured so don't bind return null; } }); } builder.mcmp().setProxyConfigurations(proxies); // SSL if (sslContextDependency != null) { builder.mcmp().setSocketFactory(sslContextDependency.getValue().getSocketFactory()); } return builder.build(); } private static boolean isMulticastEnabled(Collection<NetworkInterface> interfaces) { for (NetworkInterface iface : interfaces) { try { if (iface.isUp() && (iface.supportsMulticast() || iface.isLoopback())) { return true; } } catch (SocketException e) { // Ignore } } return false; } }