/*
* 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.test.integration.management.http;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.COMPOSITE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CONTENT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DEPLOYMENT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ENABLED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILURE_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.INPUT_STREAM_INDEX;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
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.REMOVE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STEPS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.inject.Inject;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MIME;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.AbstractContentBody;
import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.jboss.as.test.http.Authentication;
import org.jboss.dmr.ModelNode;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.FileAsset;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.wildfly.core.testrunner.ManagementClient;
import org.wildfly.core.testrunner.WildflyTestRunner;
/**
* @author Emanuel Muckenhuber
*/
@RunWith(WildflyTestRunner.class)
public class HttpGenericOperationUnitTestCase {
private static final int RANDOM_FILE_SIZE = 10 * 1024 * 1024;
private static final String HTTP_PATH = "/management-upload";
private static final String DMR_ENCODED = "application/dmr-encoded";
private static final String MANAGEMENT_REALM = "ManagementRealm";
@Inject
private ManagementClient managementClient;
private URI uri;
private CloseableHttpClient httpClient;
private File randomContent;
@Before
public void setUp() throws Exception {
final String host = managementClient.getMgmtAddress();
final int port = managementClient.getMgmtPort();
this.uri = new URI("http://" + host + ":" + port + HTTP_PATH);
httpClient = createHttpClient(host, port, Authentication.USERNAME, Authentication.PASSWORD);
randomContent = createRandomFile(RANDOM_FILE_SIZE);
}
@After
public void shutdown() throws IOException {
if (randomContent != null) {
randomContent.delete();
}
httpClient.close();
}
@Test
public void testCompositeDeploymentOperation() throws IOException {
testDeploymentOperations(2, false);
}
@Test
public void testDMREncodedOperation() throws Exception {
testDeploymentOperations(1, true);
}
/**
* Test the deployment operation. This will add and remove a given set of deployment using a composite operation and
* attaching the streams necessary to the http post message.
*
* @param quantity the amount of deployments
* @param encoded whether to send the operation in the dmr encoded format or not
* @throws IOException
*/
private void testDeploymentOperations(final int quantity, final boolean encoded) throws IOException {
// Create the deployment
final File temp = createTempDeploymentZip();
try {
final ModelNode deployment = createCompositeDeploymentOperation(quantity);
final ContentBody operation = getOperationBody(deployment, encoded);
final List<ContentBody> streams = new ArrayList<ContentBody>();
for (int i = 0; i < quantity; i++) {
streams.add(new FileBody(temp));
}
final ModelNode response = executePost(operation, encoded, streams);
Assert.assertEquals(response.toString(), SUCCESS, response.get(OUTCOME).asString());
} finally {
temp.delete();
}
// And remove the deployments again
final ModelNode remove = removeDeploymentsOperation(quantity);
final ContentBody operation = getOperationBody(remove, encoded);
final ModelNode response = executePost(operation, encoded);
Assert.assertEquals(response.toString(), SUCCESS, response.get(OUTCOME).asString());
}
private ModelNode executePost(final ContentBody operation, final boolean encoded) throws IOException {
return executePost(operation, encoded, Collections.<ContentBody>emptyList());
}
/**
* Execute the post request.
*
* @param operation the operation body
* @param encoded whether it should send the dmr encoded header
* @param streams the optional input streams
* @return the response from the server
* @throws IOException
*/
private ModelNode executePost(final ContentBody operation, final boolean encoded, final List<ContentBody> streams) throws IOException {
final HttpPost post = new HttpPost(uri);
post.setHeader("X-Management-Client-Name", "test-client");
final MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create().addPart("operation", operation);
for (ContentBody stream : streams) {
entityBuilder.addPart("input-streams", stream);
}
post.setEntity(entityBuilder.build());
return parseResponse(httpClient.execute(post), encoded);
}
private ContentBody getOperationBody(final ModelNode operation, final boolean encoded) throws IOException {
if (encoded) {
return new DMRContentEncodedBody(operation);
} else {
return new StringBody(operation.toJSONString(true), ContentType.APPLICATION_JSON);
}
}
private File createTempDeploymentZip() throws IOException {
// Reuse the test deployment and add the random content file
final JavaArchive archive = ShrinkWrap.create(JavaArchive.class, "test-http-deployment.jar")
.add(new FileAsset(randomContent), "file");
File temp = null;
try {
temp = File.createTempFile("test", "http-deployment");
archive.as(ZipExporter.class).exportTo(temp, true);
} catch (IOException e) {
if (temp != null) {
temp.delete();
}
throw e;
}
return temp;
}
private ModelNode createCompositeDeploymentOperation(final int quantity) {
final ModelNode composite = new ModelNode();
composite.get(OP).set(COMPOSITE);
composite.get(OP_ADDR).addEmptyList();
final ModelNode steps = composite.get(STEPS).setEmptyList();
for (int i = 0; i < quantity; i ++) {
final ModelNode deploymentOne = steps.add();
deploymentOne.get(OP).set(ADD);
deploymentOne.get(OP_ADDR).set(DEPLOYMENT, "deployment-" + i);
deploymentOne.get(ENABLED).set(true);
deploymentOne.get(CONTENT).add().get(INPUT_STREAM_INDEX).set(i);
}
return composite;
}
private ModelNode removeDeploymentsOperation(final int quantity) {
final ModelNode composite = new ModelNode();
composite.get(OP).set(COMPOSITE);
composite.get(OP_ADDR).addEmptyList();
final ModelNode steps = composite.get(STEPS).setEmptyList();
for (int i = 0; i < quantity; i ++) {
final ModelNode deploymentOne = steps.add();
deploymentOne.get(OP).set(REMOVE);
deploymentOne.get(OP_ADDR).set(DEPLOYMENT, "deployment-" + i);
}
return composite;
}
private ModelNode parseResponse(HttpResponse response, boolean encoded) {
try {
String content = EntityUtils.toString(response.getEntity());
int status = response.getStatusLine().getStatusCode();
ModelNode modelResponse;
if (status == HttpStatus.SC_OK) {
if (encoded) {
modelResponse = ModelNode.fromBase64(new ByteArrayInputStream(content.getBytes()));
Assert.assertTrue(response.getFirstHeader("Content-Type").getValue().contains(DMR_ENCODED));
} else {
modelResponse = ModelNode.fromJSONString(content);
}
} else {
modelResponse = new ModelNode();
modelResponse.get(OUTCOME).set(FAILED);
modelResponse.get(FAILURE_DESCRIPTION).set(content);
}
return modelResponse;
} catch (IOException e) {
throw new RuntimeException("Unable to read response content as String");
}
}
private static CloseableHttpClient createHttpClient(String host, int port, String username, String password) {
try {
SSLContext sslContext = SSLContexts.createDefault();
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslConnectionSocketFactory)
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.build();
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(new AuthScope(host, port, MANAGEMENT_REALM, AuthSchemes.DIGEST),
new UsernamePasswordCredentials(username, password));
PoolingHttpClientConnectionManager connectionPool = new PoolingHttpClientConnectionManager(registry);
HttpClientBuilder.create().setConnectionManager(connectionPool).build();
return HttpClientBuilder.create()
.setConnectionManager(connectionPool)
.setRetryHandler(new StandardHttpRequestRetryHandler(5, true))
.setDefaultCredentialsProvider(credsProvider).build();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static final Random random = new Random();
/**
* Create a file with random content and a given size.
*
* @param size the file size
* @return the created temp file
* @throws IOException
*/
private static File createRandomFile(int size) throws IOException {
final byte[] buffer = new byte[16384];
final File file = File.createTempFile("test", "artifact");
try (final FileOutputStream os = new FileOutputStream(file)) {
for (int length = 0; length < size; length += buffer.length) {
random.nextBytes(buffer);
os.write(buffer);
}
} catch (IOException e) {
file.delete();
throw e;
}
return file;
}
static class DMRContentEncodedBody extends AbstractContentBody {
private final ModelNode model;
DMRContentEncodedBody(ModelNode model) {
super(ContentType.create(DMR_ENCODED));
this.model = model;
}
@Override
public String getFilename() {
return null;
}
@Override
public void writeTo(OutputStream out) throws IOException {
model.writeBase64(out);
}
@Override
public String getCharset() {
return null;
}
@Override
public String getTransferEncoding() {
return MIME.ENC_BINARY;
}
@Override
public long getContentLength() {
// Needed for the http client
final ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
model.writeBase64(os);
return os.size();
} catch (Exception e) {
return -1;
}
}
}
}