/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, 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.jboss.as.domain.http.server;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ACCESS_MECHANISM;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CALLER_TYPE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.COMPOSITE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DOMAIN_UUID;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.EXECUTE_FOR_COORDINATOR;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OPERATION_HEADERS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OUTCOME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_OPERATION_DESCRIPTION_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_OPERATION_NAMES_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_DESCRIPTION_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SYNC_REMOVED_FOR_READD;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.USER;
import static org.jboss.as.domain.http.server.DomainUtil.writeResponse;
import static org.jboss.as.domain.http.server.logging.HttpServerLogger.ROOT_LOGGER;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.ETag;
import io.undertow.util.ETagUtils;
import io.undertow.util.HeaderMap;
import io.undertow.util.Headers;
import io.undertow.util.HexConverter;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import org.jboss.as.controller.ModelController;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.client.OperationBuilder;
import org.jboss.as.controller.client.OperationMessageHandler;
import org.jboss.as.controller.client.OperationResponse;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.core.security.AccessMechanism;
import org.jboss.as.domain.http.server.logging.HttpServerLogger;
import org.jboss.as.protocol.StreamUtils;
import org.jboss.dmr.ModelNode;
import org.xnio.IoUtils;
import org.xnio.streams.ChannelInputStream;
/**
*
* @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
*/
class DomainApiHandler implements HttpHandler {
private static final String JSON_PRETTY = "json.pretty";
private static final String USE_STREAM_AS_RESPONSE = "useStreamAsResponse";
private static final HttpString USE_STREAM_AS_RESPONSE_HEADER = new HttpString("org.wildfly.useStreamAsResponse");
/**
* Represents all possible management operations that can be executed using HTTP GET. Cacheable operations
* have a {@code maxAge} property > 0.
*/
enum GetOperation {
/*
* It is essential that the GET requests exposed over the HTTP interface are for read only
* operations that do not modify the domain model or update anything server side.
*/
RESOURCE(READ_RESOURCE_OPERATION, 0),
ATTRIBUTE("read-attribute", 0),
RESOURCE_DESCRIPTION(READ_RESOURCE_DESCRIPTION_OPERATION, Common.ONE_WEEK),
SNAPSHOTS("list-snapshots", 0),
OPERATION_DESCRIPTION(READ_OPERATION_DESCRIPTION_OPERATION, Common.ONE_WEEK),
OPERATION_NAMES(READ_OPERATION_NAMES_OPERATION, 0),
READ_CONTENT(ModelDescriptionConstants.READ_CONTENT, 0);
private String realOperation;
private int maxAge;
GetOperation(String realOperation, int maxAge) {
this.realOperation = realOperation;
this.maxAge = maxAge;
}
public String realOperation() {
return realOperation;
}
public int getMaxAge() {
return maxAge;
}
}
private final ModelController modelController;
DomainApiHandler(ModelController modelController) {
this.modelController = modelController;
}
@Override
public void handleRequest(final HttpServerExchange exchange) {
final ModelNode dmr;
final OperationResponse response;
final HeaderMap requestHeaders = exchange.getRequestHeaders();
final boolean cachable;
final boolean get = exchange.getRequestMethod().equals(Methods.GET);
final boolean encode = Common.APPLICATION_DMR_ENCODED.equals(requestHeaders.getFirst(Headers.ACCEPT))
|| Common.APPLICATION_DMR_ENCODED.equals(requestHeaders.getFirst(Headers.CONTENT_TYPE));
final OperationParameter.Builder operationParameterBuilder = new OperationParameter.Builder(get).encode(encode);
final int streamIndex = getStreamIndex(exchange, requestHeaders);
try {
if (get) {
GetOperation operation = getOperation(exchange);
operationParameterBuilder.maxAge(operation.getMaxAge());
dmr = convertGetRequest(exchange, operation);
cachable = operation.getMaxAge() > 0;
} else {
dmr = convertPostRequest(exchange, encode);
cachable = false;
}
//operationParameterBuilder.pretty(dmr.hasDefined("json.pretty") && dmr.get("json.pretty").asBoolean());
boolean pretty = false;
if (dmr.hasDefined(JSON_PRETTY)) {
String jsonPretty = dmr.get(JSON_PRETTY).asString();
pretty = jsonPretty.equals("true") || jsonPretty.equals("1");
}
operationParameterBuilder.pretty(pretty);
} catch (Exception e) {
ROOT_LOGGER.debugf("Unable to construct ModelNode '%s'", e.getMessage());
Common.sendError(exchange, false, e.toString());
return;
}
final ResponseCallback callback = new ResponseCallback() {
@Override
void doSendResponse(final OperationResponse response) {
boolean closeResponse = true;
try {
ModelNode responseNode = response.getResponseNode();
if (responseNode.hasDefined(OUTCOME) && FAILED.equals(responseNode.get(OUTCOME).asString())) {
Common.sendError(exchange, encode, responseNode);
return;
}
if (streamIndex < 0) {
writeResponse(exchange, 200, responseNode, operationParameterBuilder.build());
} else {
List<OperationResponse.StreamEntry> streamEntries = response.getInputStreams();
if (streamIndex >= streamEntries.size()) {
// invalid index
Common.sendError(exchange, encode, new ModelNode(HttpServerLogger.ROOT_LOGGER.invalidUseStreamAsResponseIndex(streamIndex, streamEntries.size())), 400);
} else {
// writeResponse will close the response
closeResponse = false;
writeResponse(exchange, 200, response, streamIndex, operationParameterBuilder.build());
}
}
} finally {
if (closeResponse) {
StreamUtils.safeClose(response);
}
}
}
};
final boolean sendPreparedResponse = sendPreparedResponse(dmr);
final ModelController.OperationTransactionControl control = sendPreparedResponse ? new ModelController.OperationTransactionControl() {
@Override
public void operationPrepared(final ModelController.OperationTransaction transaction, final ModelNode result) {
transaction.commit();
// Fix prepared result
result.get(OUTCOME).set(SUCCESS);
result.get(RESULT);
callback.sendResponse(OperationResponse.Factory.createSimple(result));
}
} : ModelController.OperationTransactionControl.COMMIT;
try {
ModelNode headers = dmr.get(OPERATION_HEADERS);
headers.get(ACCESS_MECHANISM).set(AccessMechanism.HTTP.toString());
headers.get(CALLER_TYPE).set(USER);
// Don't allow a domain-uuid operation header from a user call
if (headers.hasDefined(DOMAIN_UUID)) {
headers.remove(DOMAIN_UUID);
}
response = modelController.execute(new OperationBuilder(dmr).build(), OperationMessageHandler.logging, control);
if (cachable && streamIndex > -1) {
// Use the MD5 of the model nodes asString() method as ETag
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(response.getResponseNode().toString().getBytes(StandardCharsets.UTF_8));
ETag etag = new ETag(false, HexConverter.convertToHexString(md.digest()));
operationParameterBuilder.etag(etag);
if (!ETagUtils.handleIfNoneMatch(exchange, etag, false)) {
exchange.setStatusCode(304);
DomainUtil.writeCacheHeaders(exchange, 304, operationParameterBuilder.build());
exchange.endExchange();
return;
}
}
} catch (Throwable t) {
ROOT_LOGGER.modelRequestError(t);
Common.sendError(exchange, encode, t.getLocalizedMessage());
return;
}
callback.sendResponse(response);
}
private static int getStreamIndex(final HttpServerExchange exchange, final HeaderMap requestHeaders) {
// First check for an HTTP header
int result = getStreamIndex(requestHeaders.get(USE_STREAM_AS_RESPONSE_HEADER));
if (result == -1) {
// Nope. Now check for a URL query parameter
Map<String, Deque<String>> queryParams = exchange.getQueryParameters();
result = getStreamIndex(queryParams.get(USE_STREAM_AS_RESPONSE));
}
return result;
}
private static int getStreamIndex(Deque<String> holder) {
int result;
if (holder != null) {
if (holder.size() > 0 && holder.getFirst().length() > 0) {
result = Integer.parseInt(holder.getFirst());
} else {
result = 0;
}
} else {
result = -1;
}
return result;
}
private GetOperation getOperation(HttpServerExchange exchange) {
Map<String, Deque<String>> queryParameters = exchange.getQueryParameters();
GetOperation operation = null;
Deque<String> parameter = queryParameters.get(OP);
if (parameter != null) {
String value = parameter.getFirst();
try {
operation = GetOperation.valueOf(value.toUpperCase(Locale.ENGLISH).replace('-', '_'));
value = operation.realOperation();
} catch (Exception e) {
throw HttpServerLogger.ROOT_LOGGER.invalidOperation(e, value);
}
}
// This will now only occur if no operation at all was specified on the incoming request.
if (operation == null) {
operation = GetOperation.RESOURCE;
}
return operation;
}
private ModelNode convertGetRequest(HttpServerExchange exchange, GetOperation operation) {
ArrayList<String> pathSegments = decodePath(exchange.getRelativePath());
Map<String, Deque<String>> queryParameters = exchange.getQueryParameters();
ModelNode dmr = new ModelNode();
for (Entry<String, Deque<String>> entry : queryParameters.entrySet()) {
String key = entry.getKey();
String value = entry.getValue().getFirst();
ModelNode valueNode = null;
if (key.startsWith("operation-header-")) {
String header = key.substring("operation-header-".length());
//Remove the same headers as the native interface (ModelControllerClientOperationHandler)
if (!header.equals(SYNC_REMOVED_FOR_READD) &&
!header.equals(EXECUTE_FOR_COORDINATOR) && !header.equals(DOMAIN_UUID)) {
valueNode = dmr.get(OPERATION_HEADERS, header);
}
} else {
valueNode = dmr.get(key);
}
if (valueNode != null) {
valueNode.set(!value.equals("") ? value : "true");
}
}
dmr.get(OP).set(operation.realOperation);
ModelNode list = dmr.get(OP_ADDR).setEmptyList();
for (int i = 0; i < pathSegments.size() - 1; i += 2) {
list.add(pathSegments.get(i), pathSegments.get(i + 1));
}
return dmr;
}
private ModelNode convertPostRequest(HttpServerExchange exchange, boolean encode) throws IOException {
InputStream in = new ChannelInputStream(exchange.getRequestChannel());
try {
return encode ? ModelNode.fromBase64(in) : ModelNode.fromJSONStream(in);
} finally {
IoUtils.safeClose(in);
}
}
private ArrayList<String> decodePath(String path) {
if (path == null)
throw new IllegalArgumentException();
ArrayList<String> segments = new ArrayList<String>();
if (!path.isEmpty()) {
int i = path.charAt(0) == '/' ? 1 : 0;
do {
int j = path.indexOf('/', i);
if (j == -1)
j = path.length();
segments.add(unescape(path.substring(i, j)));
i = j + 1;
} while (i < path.length());
}
return segments;
}
private String unescape(String string) {
try {
// URLDecoder could be way more efficient, replace it one day
return URLDecoder.decode(string, Common.UTF_8);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
/**
* Determine whether the prepared response should be sent, before the operation completed. This is needed in order
* that operations like :reload() can be executed without causing communication failures.
*
* @param operation the operation to be executed
* @return {@code true} if the prepared result should be sent, {@code false} otherwise
*/
private boolean sendPreparedResponse(final ModelNode operation) {
final PathAddress address = PathAddress.pathAddress(operation.get(OP_ADDR));
final String op = operation.get(OP).asString();
final int size = address.size();
if (size == 0) {
if (op.equals("reload")) {
return true;
} else if (op.equals(COMPOSITE)) {
// TODO
return false;
} else {
return false;
}
} else if (size == 1) {
if (address.getLastElement().getKey().equals(HOST)) {
return op.equals("reload");
}
}
return false;
}
/**
* Callback to prevent the response will be sent multiple times.
*/
private abstract static class ResponseCallback {
private volatile boolean complete;
void sendResponse(final OperationResponse response) {
if (complete) {
return;
}
complete = true;
doSendResponse(response);
}
abstract void doSendResponse(OperationResponse response);
}
}