/* * JBoss, Home of Professional Open Source. * Copyright 2014, 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.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.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.RESULT; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS; 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.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Deque; import java.util.Iterator; 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.FormParserFactory; import io.undertow.util.HeaderMap; import io.undertow.util.Headers; import org.jboss.as.controller.ModelController; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.client.Operation; import org.jboss.as.controller.client.OperationBuilder; import org.jboss.as.controller.client.OperationMessageHandler; import org.jboss.as.core.security.AccessMechanism; import org.jboss.dmr.ModelNode; import org.xnio.IoUtils; /** * Generic http POST handler accepting a single operation and multiple input streams passed as part of * a {@code multipart/form-data} message. The operation is required, the attachment streams are optional. * * Content-Disposition: form-data; name="operation" * (optional) Content-Type: application/dmr-encoded * * Content-Disposition: form-data; name="..."; filename="..." * * @author Emanuel Muckenhuber */ class DomainApiGenericOperationHandler implements HttpHandler { private static final String OPERATION = "operation"; private static final String CLIENT_NAME = "X-Management-Client-Name"; private final ModelController modelController; private final FormParserFactory formParserFactory; public DomainApiGenericOperationHandler(ModelController modelController) { this.modelController = modelController; this.formParserFactory = FormParserFactory.builder().build(); } @Override public void handleRequest(final HttpServerExchange exchange) throws Exception { final FormDataParser parser = formParserFactory.createParser(exchange); if (parser == null) { Common.UNSUPPORTED_MEDIA_TYPE.handleRequest(exchange); } // Prevent CSRF which can occur from standard a multipart/form-data submission from a standard HTML form. // If the browser sends an Origin header (Chrome / Webkit) then the earlier origin check will have passed // to reach this point. If the browser doesn't (FireFox), then only requests which came from Javascript, // which enforces same-origin policy when no Origin header is present, should be allowed. The presence of // a custom header indicates usage of XHR since simple forms can not set them. HeaderMap headers = exchange.getRequestHeaders(); if (!headers.contains(Headers.ORIGIN) && !headers.contains(CLIENT_NAME)) { ROOT_LOGGER.debug("HTTP Origin or X-Management-Client-Name header is required for all multipart form data posts."); Common.UNAUTHORIZED.handleRequest(exchange); return; } // Parse the form data final FormData data = parser.parseBlocking(); final OperationParameter.Builder operationParameterBuilder = new OperationParameter.Builder(false); // Process the operation final FormData.FormValue op = data.getFirst(OPERATION); final ModelNode operation; try { String type = op.getHeaders().getFirst(Headers.CONTENT_TYPE); if (Common.APPLICATION_DMR_ENCODED.equals(type)) { try (InputStream stream = convertToStream(op)) { operation = ModelNode.fromBase64(stream); } operationParameterBuilder.encode(true); } else if (Common.APPLICATION_JSON.equals(stripSuffix(type))) { try (InputStream stream = convertToStream(op)) { operation = ModelNode.fromJSONStream(stream); } } else { ROOT_LOGGER.debug("Content-type must be application/dmr-encoded or application/json"); Common.UNAUTHORIZED.handleRequest(exchange); return; } } catch (Exception e) { ROOT_LOGGER.errorf("Unable to construct ModelNode '%s'", e.getMessage()); Common.sendError(exchange, false, e.getLocalizedMessage()); return; } // Process the input streams final OperationBuilder builder = OperationBuilder.create(operation, true); final Iterator<String> i = data.iterator(); while (i.hasNext()) { final String name = i.next(); final Deque<FormData.FormValue> contents = data.get(name); if (contents != null && !contents.isEmpty()) { for (final FormData.FormValue value : contents) { if (value.isFile()) { builder.addFileAsAttachment(value.getPath().toFile()); } } } } operationParameterBuilder.pretty(operation.hasDefined("json.pretty") && operation.get("json.pretty").asBoolean()); // Response callback to handle the :reload operation final OperationParameter opParam = operationParameterBuilder.build(); final ResponseCallback callback = new ResponseCallback() { @Override void doSendResponse(final ModelNode response) { if (response.hasDefined(OUTCOME) && FAILED.equals(response.get(OUTCOME).asString())) { Common.sendError(exchange, opParam.isEncode(), response); return; } writeResponse(exchange, 200, response, opParam); } }; final boolean sendPreparedResponse = sendPreparedResponse(operation); 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(result); } } : ModelController.OperationTransactionControl.COMMIT; ModelNode response; final Operation builtOp = builder.build(); try { ModelNode opheaders = operation.get(OPERATION_HEADERS); opheaders.get(ACCESS_MECHANISM).set(AccessMechanism.HTTP.toString()); opheaders.get(CALLER_TYPE).set(USER); // Don't allow a domain-uuid operation header from a user call if (opheaders.hasDefined(DOMAIN_UUID)) { opheaders.remove(DOMAIN_UUID); } response = modelController.execute(operation, OperationMessageHandler.DISCARD, control, builtOp); } catch (Throwable t) { ROOT_LOGGER.modelRequestError(t); Common.sendError(exchange, opParam.isEncode(), t.getLocalizedMessage()); return; } finally { // Close any input streams that were open if (builtOp.isAutoCloseStreams()) { for (InputStream in : builtOp.getInputStreams()) { IoUtils.safeClose(in); } } } callback.sendResponse(response); } private InputStream convertToStream(FormData.FormValue op) throws IOException { if (op.isFile()) { return Files.newInputStream(op.getPath()); } else { return new ByteArrayInputStream(op.getValue().getBytes(StandardCharsets.UTF_8)); } } private static String stripSuffix(String contentType) { if (contentType == null) { return null; } int index = contentType.indexOf(';'); if (index > 0) { contentType = contentType.substring(0, index); } return contentType; } static final String RELOAD = "reload"; /** * 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 boolean complete; synchronized void sendResponse(final ModelNode response) { if (complete) { return; } complete = true; doSendResponse(response); } abstract void doSendResponse(ModelNode response); } }