/* * 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); } }