/*
* JBoss, Home of Professional Open Source.
* Copyright 2017, 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.wildfly.extension.undertow;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import io.undertow.Handlers;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.PathHandler;
import io.undertow.servlet.api.Deployment;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.util.CopyOnWriteMap;
import io.undertow.util.Methods;
import org.jboss.as.controller.ControlledProcessState;
import org.jboss.as.controller.ControlledProcessStateService;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.wildfly.extension.undertow.deployment.GateHandlerWrapper;
import org.wildfly.extension.undertow.logging.UndertowLogger;
/**
* @author <a href="mailto:tomaz.cerar@redhat.com">Tomaz Cerar</a> (c) 2013 Red Hat Inc.
* @author Radoslav Husar
*/
public class Host implements Service<Host>, FilterLocation {
private final PathHandler pathHandler = new PathHandler();
private volatile HttpHandler rootHandler = null;
private final Set<String> allAliases;
private final String name;
private final String defaultWebModule;
private final InjectedValue<Server> server = new InjectedValue<>();
private final InjectedValue<UndertowService> undertowService = new InjectedValue<>();
private volatile AccessLogService accessLogService;
private final List<UndertowFilter> filters = new CopyOnWriteArrayList<>();
private final Set<Deployment> deployments = new CopyOnWriteArraySet<>();
private final Map<String, LocationService> locations = new CopyOnWriteMap<>();
private final Map<String, AuthenticationMechanism> additionalAuthenticationMechanisms = new ConcurrentHashMap<>();
private final HostRootHandler hostRootHandler = new HostRootHandler();
private final InjectedValue<ControlledProcessStateService> controlledProcessStateServiceInjectedValue = new InjectedValue<>();
private volatile GateHandlerWrapper gateHandlerWrapper;
private final DefaultResponseCodeHandler defaultHandler;
public Host(final String name, final List<String> aliases, final String defaultWebModule, final int defaultResponseCode ) {
this.name = name;
this.defaultWebModule = defaultWebModule;
Set<String> hosts = new HashSet<>(aliases.size() + 1);
hosts.add(name);
hosts.addAll(aliases);
allAliases = Collections.unmodifiableSet(hosts);
this.defaultHandler = new DefaultResponseCodeHandler(defaultResponseCode);
this.setupDefaultResponseCodeHandler();
}
public Host(final String name, final List<String> aliases, final String defaultWebModule ) {
this.name = name;
this.defaultWebModule = defaultWebModule;
Set<String> hosts = new HashSet<>(aliases.size() + 1);
hosts.add(name);
hosts.addAll(aliases);
this.allAliases = Collections.unmodifiableSet(hosts);
this.defaultHandler = null;
}
private String getDeployedContextPath(DeploymentInfo deploymentInfo) {
return "".equals(deploymentInfo.getContextPath()) ? "/" : deploymentInfo.getContextPath();
}
@Override
public void start(StartContext context) throws StartException {
ControlledProcessStateService controlledProcessStateService = controlledProcessStateServiceInjectedValue.getValue();
//may be null for tests
if(controlledProcessStateService != null && controlledProcessStateService.getCurrentState() == ControlledProcessState.State.STARTING) {
gateHandlerWrapper = new GateHandlerWrapper();
controlledProcessStateService.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
controlledProcessStateService.removePropertyChangeListener(this);
if(gateHandlerWrapper != null) {
gateHandlerWrapper.open();
gateHandlerWrapper = null;
}
rootHandler = null;
}
});
}
server.getValue().registerHost(this);
UndertowLogger.ROOT_LOGGER.hostStarting(name);
}
private HttpHandler configureRootHandler() {
AccessLogService logService = accessLogService;
HttpHandler rootHandler = pathHandler;
ArrayList<UndertowFilter> filters = new ArrayList<>(this.filters);
//handle options * requests
rootHandler = new OptionsHandler(rootHandler);
//handle requests that use the Expect: 100-continue header
rootHandler = Handlers.httpContinueRead(rootHandler);
rootHandler = LocationService.configureHandlerChain(rootHandler, filters);
if (logService != null) {
rootHandler = logService.configureAccessLogHandler(rootHandler);
}
GateHandlerWrapper gateHandlerWrapper = this.gateHandlerWrapper;
if(gateHandlerWrapper != null) {
rootHandler = gateHandlerWrapper.wrap(rootHandler);
}
return rootHandler;
}
@Override
public void stop(StopContext context) {
server.getValue().unregisterHost(this);
pathHandler.clearPaths();
if(gateHandlerWrapper != null) {
gateHandlerWrapper.open();
gateHandlerWrapper = null;
}
UndertowLogger.ROOT_LOGGER.hostStopping(name);
}
@Override
public Host getValue() throws IllegalStateException, IllegalArgumentException {
return this;
}
protected InjectedValue<Server> getServerInjection() {
return server;
}
void setAccessLogService(AccessLogService service) {
this.accessLogService = service;
rootHandler = null;
}
public Server getServer() {
return server.getValue();
}
protected InjectedValue<UndertowService> getUndertowService() {
return undertowService;
}
public Set<String> getAllAliases() {
return allAliases;
}
public String getName() {
return name;
}
protected HttpHandler getRootHandler() {
return hostRootHandler;
}
List<UndertowFilter> getFilters() {
return Collections.unmodifiableList(filters);
}
protected HttpHandler getOrCreateRootHandler() {
HttpHandler root = rootHandler;
if(root == null) {
synchronized (this) {
root = rootHandler;
if(root == null) {
return rootHandler = configureRootHandler();
}
}
}
return root;
}
public String getDefaultWebModule() {
return defaultWebModule;
}
public void registerDeployment(final Deployment deployment, HttpHandler handler) {
DeploymentInfo deploymentInfo = deployment.getDeploymentInfo();
String path = getDeployedContextPath(deploymentInfo);
registerHandler(path, handler);
deployments.add(deployment);
UndertowLogger.ROOT_LOGGER.registerWebapp(path, getServer().getName());
undertowService.getValue().fireEvent(listener -> listener.onDeploymentStart(deployment, Host.this));
}
public void unregisterDeployment(final Deployment deployment) {
DeploymentInfo deploymentInfo = deployment.getDeploymentInfo();
String path = getDeployedContextPath(deploymentInfo);
undertowService.getValue().fireEvent(listener -> listener.onDeploymentStop(deployment, Host.this));
unregisterHandler(path);
deployments.remove(deployment);
UndertowLogger.ROOT_LOGGER.unregisterWebapp(path, getServer().getName());
}
void registerLocation(String path) {
String realPath = path.startsWith("/") ? path : "/" + path;
locations.put(realPath, null);
undertowService.getValue().fireEvent(listener -> listener.onDeploymentStart(realPath, Host.this));
}
void unregisterLocation(String path) {
String realPath = path.startsWith("/") ? path : "/" + path;
locations.remove(realPath);
undertowService.getValue().fireEvent(listener -> listener.onDeploymentStop(realPath, Host.this));
}
public void registerHandler(String path, HttpHandler handler) {
pathHandler.addPrefixPath(path, handler);
}
public void unregisterHandler(String path) {
pathHandler.removePrefixPath(path);
// if there is registered location for given path, serve it from now on
LocationService location = locations.get(path);
if (location != null) {
pathHandler.addPrefixPath(location.getLocationPath(), location.getLocationHandler());
}
// else serve the default response code
else if (path.equals("/")) {
this.setupDefaultResponseCodeHandler();
}
}
void registerLocation(LocationService location) {
locations.put(location.getLocationPath(), location);
registerHandler(location.getLocationPath(), location.getLocationHandler());
undertowService.getValue().fireEvent(listener -> listener.onDeploymentStart(location.getLocationPath(), Host.this));
}
void unregisterLocation(LocationService location) {
locations.remove(location.getLocationPath());
unregisterHandler(location.getLocationPath());
undertowService.getValue().fireEvent(listener -> listener.onDeploymentStop(location.getLocationPath(), Host.this));
}
public Set<String> getLocations() {
return Collections.unmodifiableSet(locations.keySet());
}
/**
* @return set of currently registered {@link Deployment}s on this host
*/
public Set<Deployment> getDeployments() {
return Collections.unmodifiableSet(deployments);
}
void registerAdditionalAuthenticationMechanism(String name, AuthenticationMechanism authenticationMechanism){
additionalAuthenticationMechanisms.put(name, authenticationMechanism);
}
void unregisterAdditionalAuthenticationMechanism(String name){
additionalAuthenticationMechanisms.remove(name);
}
public Map<String, AuthenticationMechanism> getAdditionalAuthenticationMechanisms() {
return new LinkedHashMap<>(additionalAuthenticationMechanisms);
}
@Override
public void addFilter(UndertowFilter filterRef) {
filters.add(filterRef);
rootHandler = null;
}
@Override
public void removeFilter(UndertowFilter filterRef) {
filters.remove(filterRef);
rootHandler = null;
}
protected void setupDefaultResponseCodeHandler(){
if(this.defaultHandler != null){
this.registerHandler("/", this.defaultHandler);
}
}
InjectedValue<ControlledProcessStateService> getControlledProcessStateServiceInjectedValue() {
return controlledProcessStateServiceInjectedValue;
}
private static final class OptionsHandler implements HttpHandler {
private final HttpHandler next;
private OptionsHandler(HttpHandler next) {
this.next = next;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
if(exchange.getRequestMethod().equals(Methods.OPTIONS) &&
exchange.getRelativePath().equals("*")) {
//handle the OPTIONS requests
//basically just return an empty response
exchange.endExchange();
return;
}
next.handleRequest(exchange);
}
}
private class HostRootHandler implements HttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
getOrCreateRootHandler().handleRequest(exchange);
}
}
}