package io.undertow.server.handlers.proxy.mod_cluster;
import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.Version;
import io.undertow.io.Sender;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormData;
import io.undertow.server.handlers.form.FormDataParser;
import io.undertow.server.handlers.form.FormEncodedDataDefinition;
import io.undertow.server.handlers.form.FormParserFactory;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.StatusCodes;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.ssl.XnioSsl;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.ALIAS;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.BALANCER;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.CONTEXT;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.DOMAIN;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.FLUSH_PACKET;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.FLUSH_WAIT;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.HOST;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.JVMROUTE;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.LOAD;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.MAXATTEMPTS;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.PORT;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.REVERSED;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.SCHEME;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.SMAX;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSION;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSIONCOOKIE;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSIONFORCE;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSIONPATH;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSIONREMOVE;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.TIMEOUT;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.TTL;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.TYPE;
import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.WAITWORKER;
class MCMPHandler implements HttpHandler {
enum MCMPAction {
ENABLE,
DISABLE,
STOP,
REMOVE,
;
}
public static final HttpString CONFIG = new HttpString("CONFIG");
public static final HttpString ENABLE_APP = new HttpString("ENABLE-APP");
public static final HttpString DISABLE_APP = new HttpString("DISABLE-APP");
public static final HttpString STOP_APP = new HttpString("STOP-APP");
public static final HttpString REMOVE_APP = new HttpString("REMOVE-APP");
public static final HttpString STATUS = new HttpString("STATUS");
public static final HttpString DUMP = new HttpString("DUMP");
public static final HttpString INFO = new HttpString("INFO");
public static final HttpString PING = new HttpString("PING");
private static final Set<HttpString> HANDLED_METHODS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(CONFIG, ENABLE_APP, DISABLE_APP, STOP_APP, REMOVE_APP, STATUS, INFO, DUMP, PING)));
protected static final String VERSION_PROTOCOL = "0.2.1";
protected static final String MOD_CLUSTER_EXPOSED_VERSION = "mod_cluster_undertow/" + Version.getVersionString();
private static final String CONTENT_TYPE = "text/plain; charset=ISO-8859-1";
private static final String TYPESYNTAX = MCMPConstants.TYPESYNTAX;
private static final String SCONBAD = "SYNTAX: Context without Alias";
private static final String SBADFLD = "SYNTAX: Invalid field ";
private static final String SBADFLD1 = " in message";
private static final String SMISFLD = "SYNTAX: Mandatory field(s) missing in message";
private final FormParserFactory parserFactory;
private final MCMPConfig config;
private final HttpHandler next;
private final long creationTime = System.currentTimeMillis();
private final ModCluster modCluster;
protected final ModClusterContainer container;
MCMPHandler(MCMPConfig config, ModCluster modCluster, HttpHandler next) {
this.config = config;
this.next = next;
this.modCluster = modCluster;
this.container = modCluster.getContainer();
this.parserFactory = FormParserFactory.builder(false).addParser(new FormEncodedDataDefinition().setForceCreation(true)).build();
UndertowLogger.ROOT_LOGGER.mcmpHandlerCreated();
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
final HttpString method = exchange.getRequestMethod();
if(!handlesMethod(method)) {
next.handleRequest(exchange);
return;
}
final InetSocketAddress addr = exchange.getConnection().getLocalAddress(InetSocketAddress.class);
if (!addr.isUnresolved() && addr.getPort() != config.getManagementSocketAddress().getPort() || !Arrays.equals(addr.getAddress().getAddress(), config.getManagementSocketAddress().getAddress().getAddress())) {
next.handleRequest(exchange);
return;
}
if(exchange.isInIoThread()) {
exchange.dispatch(this);
return;
}
try {
handleRequest(method, exchange);
} catch (Exception e) {
UndertowLogger.ROOT_LOGGER.failedToProcessManagementReq(e);
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, CONTENT_TYPE);
final Sender sender = exchange.getResponseSender();
sender.send("failed to process management request");
}
}
protected boolean handlesMethod(HttpString method) {
return HANDLED_METHODS.contains(method);
}
protected void handleRequest(final HttpString method, HttpServerExchange exchange) throws Exception {
final RequestData requestData = parseFormData(exchange);
boolean persistent = exchange.isPersistent();
exchange.setPersistent(false);
if (CONFIG.equals(method)) {
processConfig(exchange, requestData);
} else if (ENABLE_APP.equals(method)) {
processCommand(exchange, requestData, MCMPAction.ENABLE);
} else if (DISABLE_APP.equals(method)) {
processCommand(exchange, requestData, MCMPAction.DISABLE);
} else if (STOP_APP.equals(method)) {
processCommand(exchange, requestData, MCMPAction.STOP);
} else if (REMOVE_APP.equals(method)) {
processCommand(exchange, requestData, MCMPAction.REMOVE);
} else if (STATUS.equals(method)) {
processStatus(exchange, requestData);
} else if (INFO.equals(method)) {
processInfo(exchange);
} else if (DUMP.equals(method)) {
processDump(exchange);
} else if (PING.equals(method)) {
processPing(exchange, requestData);
} else {
exchange.setPersistent(persistent);
next.handleRequest(exchange);
}
}
private void processConfig(final HttpServerExchange exchange, final RequestData requestData) {
List<String> hosts = null;
List<String> contexts = null;
final Balancer.BalancerBuilder balancer = Balancer.builder();
final NodeConfig.NodeBuilder node = NodeConfig.builder(modCluster);
final Iterator<HttpString> i = requestData.iterator();
while (i.hasNext()) {
final HttpString name = i.next();
final String value = requestData.getFirst(name);
UndertowLogger.ROOT_LOGGER.mcmpKeyValue(name, value);
if (!checkString(value)) {
processError(TYPESYNTAX, SBADFLD + name + SBADFLD1, exchange);
return;
}
if (BALANCER.equals(name)) {
node.setBalancer(value);
balancer.setName(value);
} else if (MAXATTEMPTS.equals(name)) {
balancer.setMaxRetries(Integer.parseInt(value));
} else if (STICKYSESSION.equals(name)) {
if ("No".equalsIgnoreCase(value)) {
balancer.setStickySession(false);
}
} else if (STICKYSESSIONCOOKIE.equals(name)) {
balancer.setStickySessionCookie(value);
} else if (STICKYSESSIONPATH.equals(name)) {
balancer.setStickySessionPath(value);
} else if (STICKYSESSIONREMOVE.equals(name)) {
if ("Yes".equalsIgnoreCase(value)) {
balancer.setStickySessionRemove(true);
}
} else if (STICKYSESSIONFORCE.equals(name)) {
if ("no".equalsIgnoreCase(value)) {
balancer.setStickySessionForce(false);
}
} else if (JVMROUTE.equals(name)) {
node.setJvmRoute(value);
} else if (DOMAIN.equals(name)) {
node.setDomain(value);
} else if (HOST.equals(name)) {
node.setHostname(value);
} else if (PORT.equals(name)) {
node.setPort(Integer.parseInt(value));
} else if (TYPE.equals(name)) {
node.setType(value);
} else if (REVERSED.equals(name)) {
continue;
} else if (FLUSH_PACKET.equals(name)) {
if ("on".equalsIgnoreCase(value)) {
node.setFlushPackets(true);
} else if ("auto".equalsIgnoreCase(value)) {
node.setFlushPackets(true);
}
} else if (FLUSH_WAIT.equals(name)) {
node.setFlushwait(Integer.parseInt(value));
} else if (MCMPConstants.PING.equals(name)) {
node.setPing(Integer.parseInt(value));
} else if (SMAX.equals(name)) {
node.setSmax(Integer.parseInt(value));
} else if (TTL.equals(name)) {
node.setTtl(TimeUnit.SECONDS.toMillis(Long.parseLong(value)));
} else if (TIMEOUT.equals(name)) {
node.setTimeout(Integer.parseInt(value));
} else if (CONTEXT.equals(name)) {
final String[] context = value.split(",");
contexts = Arrays.asList(context);
} else if (ALIAS.equals(name)) {
final String[] alias = value.split(",");
hosts = Arrays.asList(alias);
} else if(WAITWORKER.equals(name)) {
node.setWaitWorker(Integer.parseInt(value));
} else {
processError(TYPESYNTAX, SBADFLD + name + SBADFLD1, exchange);
return;
}
}
final NodeConfig config;
try {
config = node.build();
if (container.addNode(config, balancer, exchange.getIoThread(), exchange.getConnection().getByteBufferPool())) {
if (contexts != null && hosts != null) {
for (final String context : contexts) {
container.enableContext(context, config.getJvmRoute(), hosts);
}
}
processOK(exchange);
} else {
processError(MCMPErrorCode.NODE_STILL_EXISTS, exchange);
}
} catch (Exception e) {
processError(MCMPErrorCode.CANT_UPDATE_NODE, exchange);
}
}
void processCommand(final HttpServerExchange exchange, final RequestData requestData, final MCMPAction action) throws IOException {
if (exchange.getRequestPath().equals("*") || exchange.getRequestPath().endsWith("/*")) {
processNodeCommand(exchange, requestData, action);
} else {
processAppCommand(exchange, requestData, action);
}
}
void processNodeCommand(final HttpServerExchange exchange, final RequestData requestData, final MCMPAction action) throws IOException {
final String jvmRoute = requestData.getFirst(JVMROUTE);
if (jvmRoute == null) {
processError(TYPESYNTAX, SMISFLD, exchange);
return;
}
if (processNodeCommand(jvmRoute, action)) {
processOK(exchange);
} else {
processError(MCMPErrorCode.CANT_UPDATE_NODE, exchange);
}
}
boolean processNodeCommand(final String jvmRoute, final MCMPAction action) throws IOException {
switch (action) {
case ENABLE:
return container.enableNode(jvmRoute);
case DISABLE:
return container.disableNode(jvmRoute);
case STOP:
return container.stopNode(jvmRoute);
case REMOVE:
return container.removeNode(jvmRoute) != null;
}
return false;
}
void processAppCommand(final HttpServerExchange exchange, final RequestData requestData, final MCMPAction action) throws IOException {
final String contextPath = requestData.getFirst(CONTEXT);
final String jvmRoute = requestData.getFirst(JVMROUTE);
final String aliases = requestData.getFirst(ALIAS);
if (contextPath == null || jvmRoute == null || aliases == null) {
processError(TYPESYNTAX, SMISFLD, exchange);
return;
}
final List<String> virtualHosts = Arrays.asList(aliases.split(","));
if (virtualHosts == null || virtualHosts.isEmpty()) {
processError(TYPESYNTAX, SCONBAD, exchange);
return;
}
String response = null;
switch (action) {
case ENABLE:
if (!container.enableContext(contextPath, jvmRoute, virtualHosts)) {
processError(MCMPErrorCode.CANT_UPDATE_CONTEXT, exchange);
return;
}
break;
case DISABLE:
if (!container.disableContext(contextPath, jvmRoute, virtualHosts)) {
processError(MCMPErrorCode.CANT_UPDATE_CONTEXT, exchange);
return;
}
break;
case STOP:
int i = container.stopContext(contextPath, jvmRoute, virtualHosts);
final StringBuilder builder = new StringBuilder();
builder.append("Type=STOP-APP-RSP,JvmRoute=").append(jvmRoute);
builder.append("Alias=").append(aliases);
builder.append("Context=").append(contextPath);
builder.append("Requests=").append(i);
response = builder.toString();
break;
case REMOVE:
if (!container.removeContext(contextPath, jvmRoute, virtualHosts)) {
processError(MCMPErrorCode.CANT_UPDATE_CONTEXT, exchange);
return;
}
break;
default: {
processError(TYPESYNTAX, SMISFLD, exchange);
return;
}
}
if (response != null) {
sendResponse(exchange, response);
} else {
processOK(exchange);
}
}
void processStatus(final HttpServerExchange exchange, final RequestData requestData) throws IOException {
final String jvmRoute = requestData.getFirst(JVMROUTE);
final String loadValue = requestData.getFirst(LOAD);
if (loadValue == null || jvmRoute == null) {
processError(TYPESYNTAX, SMISFLD, exchange);
return;
}
UndertowLogger.ROOT_LOGGER.receivedNodeLoad(jvmRoute, loadValue);
final int load = Integer.parseInt(loadValue);
if (load > 0 || load == -2) {
final Node node = container.getNode(jvmRoute);
if (node == null) {
processError(MCMPErrorCode.CANT_READ_NODE, exchange);
return;
}
final NodePingUtil.PingCallback callback = new NodePingUtil.PingCallback() {
@Override
public void completed() {
final String response = "Type=STATUS-RSP&State=OK&JVMRoute=" + jvmRoute + "&id=" + creationTime;
try {
if (load > 0) {
node.updateLoad(load);
}
sendResponse(exchange, response);
} catch (Exception e) {
UndertowLogger.ROOT_LOGGER.failedToSendPingResponse(e);
}
}
@Override
public void failed() {
final String response = "Type=STATUS-RSP&State=NOTOK&JVMRoute=" + jvmRoute + "&id=" + creationTime;
try {
node.markInError();
sendResponse(exchange, response);
} catch (Exception e) {
UndertowLogger.ROOT_LOGGER.failedToSendPingResponseDBG(e, node.getJvmRoute(), jvmRoute);
}
}
};
node.ping(exchange, callback);
} else if (load == 0) {
final Node node = container.getNode(jvmRoute);
if (node != null) {
node.hotStandby();
sendResponse(exchange, "Type=STATUS-RSP&State=OK&JVMRoute=" + jvmRoute + "&id=" + creationTime);
} else {
processError(MCMPErrorCode.CANT_READ_NODE, exchange);
}
} else if (load == -1) {
final Node node = container.getNode(jvmRoute);
if (node != null) {
node.markInError();
sendResponse(exchange, "Type=STATUS-RSP&State=NOTOK&JVMRoute=" + jvmRoute + "&id=" + creationTime);
} else {
processError(MCMPErrorCode.CANT_READ_NODE, exchange);
}
} else {
processError(TYPESYNTAX, SMISFLD, exchange);
}
}
void processPing(final HttpServerExchange exchange, final RequestData requestData) throws IOException {
final String jvmRoute = requestData.getFirst(JVMROUTE);
final String scheme = requestData.getFirst(SCHEME);
final String host = requestData.getFirst(HOST);
final String port = requestData.getFirst(PORT);
final String OK = "Type=PING-RSP&State=OK&id=" + creationTime;
final String NOTOK = "Type=PING-RSP&State=NOTOK&id=" + creationTime;
if (jvmRoute != null) {
final Node nodeConfig = container.getNode(jvmRoute);
if (nodeConfig == null) {
sendResponse(exchange, NOTOK);
return;
}
final NodePingUtil.PingCallback callback = new NodePingUtil.PingCallback() {
@Override
public void completed() {
try {
sendResponse(exchange, OK);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed() {
try {
nodeConfig.markInError();
sendResponse(exchange, NOTOK);
} catch (Exception e) {
e.printStackTrace();
}
}
};
nodeConfig.ping(exchange, callback);
} else {
if (scheme == null && host == null && port == null) {
sendResponse(exchange, OK);
return;
} else {
if (host == null || port == null) {
processError(TYPESYNTAX, SMISFLD, exchange);
return;
}
checkHostUp(scheme, host, Integer.parseInt(port), exchange, new NodePingUtil.PingCallback() {
@Override
public void completed() {
sendResponse(exchange, OK);
}
@Override
public void failed() {
sendResponse(exchange, NOTOK);
}
});
return;
}
}
}
protected void processInfo(HttpServerExchange exchange) throws IOException {
final String data = processInfoString();
exchange.getResponseHeaders().add(Headers.SERVER, MOD_CLUSTER_EXPOSED_VERSION);
sendResponse(exchange, data);
}
protected String processInfoString() {
final StringBuilder builder = new StringBuilder();
final List<Node.VHostMapping> vHosts = new ArrayList<>();
final List<Context> contexts = new ArrayList<>();
final Collection<Node> nodes = container.getNodes();
for (final Node node : nodes) {
MCMPInfoUtil.printInfo(node, builder);
vHosts.addAll(node.getVHosts());
contexts.addAll(node.getContexts());
}
for (final Node.VHostMapping vHost : vHosts) {
MCMPInfoUtil.printInfo(vHost, builder);
}
for (final Context context : contexts) {
MCMPInfoUtil.printInfo(context, builder);
}
return builder.toString();
}
protected void processDump(HttpServerExchange exchange) throws IOException {
final String data = processDumpString();
exchange.getResponseHeaders().add(Headers.SERVER, MOD_CLUSTER_EXPOSED_VERSION);
sendResponse(exchange, data);
}
protected String processDumpString() {
final StringBuilder builder = new StringBuilder();
final Collection<Balancer> balancers = container.getBalancers();
for (final Balancer balancer : balancers) {
MCMPInfoUtil.printDump(balancer, builder);
}
final List<Node.VHostMapping> vHosts = new ArrayList<>();
final List<Context> contexts = new ArrayList<>();
final Collection<Node> nodes = container.getNodes();
for (final Node node : nodes) {
MCMPInfoUtil.printDump(node, builder);
vHosts.addAll(node.getVHosts());
contexts.addAll(node.getContexts());
}
for (final Node.VHostMapping vHost : vHosts) {
MCMPInfoUtil.printDump(vHost, builder);
}
for (final Context context : contexts) {
MCMPInfoUtil.printDump(context, builder);
}
return builder.toString();
}
protected void checkHostUp(final String scheme, final String host, final int port, final HttpServerExchange exchange, final NodePingUtil.PingCallback callback) {
final XnioSsl xnioSsl = null;
final OptionMap options = OptionMap.builder()
.set(Options.TCP_NODELAY, true)
.getMap();
try {
if ("ajp".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)) {
final URI uri = new URI(scheme, null, host, port, "/", null, null);
NodePingUtil.pingHttpClient(uri, callback, exchange, container.getClient(), xnioSsl, options);
} else {
final InetSocketAddress address = new InetSocketAddress(host, port);
NodePingUtil.pingHost(address, exchange, callback, options);
}
} catch (URISyntaxException e) {
callback.failed();
}
}
static void sendResponse(final HttpServerExchange exchange, final String response) {
exchange.setStatusCode(StatusCodes.OK);
exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, CONTENT_TYPE);
final Sender sender = exchange.getResponseSender();
UndertowLogger.ROOT_LOGGER.mcmpSendingResponse(exchange.getSourceAddress(), exchange.getStatusCode(), exchange.getResponseHeaders(), response);
sender.send(response);
}
static void processOK(HttpServerExchange exchange) throws IOException {
exchange.setStatusCode(StatusCodes.OK);
exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, CONTENT_TYPE);
exchange.endExchange();
}
static void processError(MCMPErrorCode errorCode, HttpServerExchange exchange) {
processError(errorCode.getType(), errorCode.getMessage(), exchange);
}
static void processError(String type, String errString, HttpServerExchange exchange) {
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, CONTENT_TYPE);
exchange.getResponseHeaders().add(new HttpString("Version"), VERSION_PROTOCOL);
exchange.getResponseHeaders().add(new HttpString("Type"), type);
exchange.getResponseHeaders().add(new HttpString("Mess"), errString);
exchange.endExchange();
UndertowLogger.ROOT_LOGGER.mcmpProcessingError(type, errString);
}
RequestData parseFormData(final HttpServerExchange exchange) throws IOException {
final FormDataParser parser = parserFactory.createParser(exchange);
final FormData formData = parser.parseBlocking();
final RequestData data = new RequestData();
for (String name : formData) {
final HttpString key = new HttpString(name);
data.add(key, formData.get(name));
}
return data;
}
private static void checkStringForSuspiciousCharacters(String data) {
for(int i = 0; i < data.length(); ++i) {
char c = data.charAt(i);
if(c == '>' || c == '<' || c == '\\' || c == '\"' || c == '\n' || c == '\r') {
throw UndertowMessages.MESSAGES.mcmpMessageRejectedDueToSuspiciousCharacters(data);
}
}
}
static class RequestData {
private final Map<HttpString, Deque<String>> values = new LinkedHashMap<>();
Iterator<HttpString> iterator() {
return values.keySet().iterator();
}
void add(final HttpString name, Deque<FormData.FormValue> values) {
checkStringForSuspiciousCharacters(name.toString());
for (final FormData.FormValue value : values) {
add(name, value);
}
}
void addValues(final HttpString name, Deque<String> value) {
Deque<String> values = this.values.get(name);
for(String i : value) {
checkStringForSuspiciousCharacters(i);
}
if (values == null) {
this.values.put(name, value);
} else {
values.addAll(value);
}
}
void add(final HttpString name, final FormData.FormValue value) {
Deque<String> values = this.values.get(name);
if (values == null) {
this.values.put(name, values = new ArrayDeque<>(1));
}
String stringVal = value.getValue();
checkStringForSuspiciousCharacters(stringVal);
values.add(stringVal);
}
String getFirst(HttpString name) {
final Deque<String> deque = values.get(name);
return deque == null ? null : deque.peekFirst();
}
}
static boolean checkString(final String value) {
return value != null && value.length() > 0;
}
}