/**
* Licensed to jclouds, Inc. (jclouds) under one or more
* contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. jclouds licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jclouds.http;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static com.google.common.hash.Hashing.md5;
import static com.google.common.io.BaseEncoding.base64;
import static com.google.common.io.ByteStreams.copy;
import static com.google.common.io.ByteStreams.join;
import static com.google.common.io.ByteStreams.newInputStreamSupplier;
import static com.google.common.io.ByteStreams.toByteArray;
import static com.google.common.io.Closeables.closeQuietly;
import static com.google.common.net.HttpHeaders.CONTENT_DISPOSITION;
import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
import static com.google.common.net.HttpHeaders.CONTENT_LANGUAGE;
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.jclouds.Constants.PROPERTY_RELAX_HOSTNAME;
import static org.jclouds.Constants.PROPERTY_TRUST_ALL_CERTS;
import static org.jclouds.io.ByteSources.asByteSource;
import static org.jclouds.util.Strings2.toStringAndClose;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.jclouds.ContextBuilder;
import org.jclouds.providers.AnonymousProviderMetadata;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Optional;
import org.testng.annotations.Parameters;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.io.InputSupplier;
import com.google.inject.Injector;
import com.google.inject.Module;
public abstract class BaseJettyTest {
protected static final String XML = "<foo><bar>whoppers</bar></foo>";
protected static final String XML2 = "<foo><bar>chubbs</bar></foo>";
protected Server server = null;
protected IntegrationTestClient client;
protected Injector injector;
private AtomicInteger cycle = new AtomicInteger(0);
private Server server2;
protected int testPort;
protected String md5;
static final Pattern actionPattern = Pattern.compile("/objects/(.*)/action/([a-z]*);?(.*)");
@BeforeClass
@Parameters({ "test-jetty-port" })
public void setUpJetty(@Optional("8123") final int testPort) throws Exception {
this.testPort = testPort;
final InputSupplier<InputStream> oneHundredOneConstitutions = getTestDataSupplier();
md5 = base64().encode(asByteSource(oneHundredOneConstitutions.getInput()).hash(md5()).asBytes());
Handler server1Handler = new AbstractHandler() {
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
if (failIfNoContentLength(request, response)) {
return;
} else if (target.indexOf("sleep") > 0) {
sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
response.setContentType("text/xml");
response.setStatus(SC_OK);
} else if (target.indexOf("redirect") > 0) {
// in OpenJDK 7.0, expect continue handling is enforced, so we
// have to consume the stream.
// http://hg.openjdk.java.net/jdk7/tl/jdk/rev/045aeb76b0ff
// getInputStream address the expect-continue, per jetty docs
// http://wiki.eclipse.org/Jetty/Feature/1xx_Responses#100_Continue
toStringAndClose(request.getInputStream());
response.sendRedirect("https://localhost:" + (testPort + 1) + "/");
} else if (target.indexOf("101constitutions") > 0) {
response.setContentType("text/plain");
response.setHeader("Content-MD5", md5);
response.setStatus(SC_OK);
copy(oneHundredOneConstitutions, response.getOutputStream());
} else if (request.getMethod().equals("PUT")) {
if (request.getContentLength() > 0) {
response.setStatus(SC_OK);
response.getWriter().println(toStringAndClose(request.getInputStream()) + "PUT");
} else {
response.setStatus(SC_OK);
}
} else if (request.getMethod().equals("POST")) {
// don't redirect large objects
if (request.getContentLength() < 10240 && redirectEveryTwentyRequests(request, response))
return;
if (failEveryTenRequests(request, response))
return;
if (request.getContentLength() > 0) {
handlePost(request, response);
} else {
handleAction(request, response);
}
} else if (request.getHeader("range") != null) {
response.sendError(404, "no content");
} else if (request.getHeader("test") != null) {
response.setContentType("text/plain");
response.setStatus(SC_OK);
response.getWriter().println("test");
} else if (request.getMethod().equals("HEAD")) {
// by HTML specification, HEAD response MUST NOT include a body
response.setContentType("text/xml");
response.setStatus(SC_OK);
} else {
if (failEveryTenRequests(request, response))
return;
response.setContentType("text/xml");
response.setStatus(SC_OK);
response.getWriter().println(XML);
}
Request.class.cast(request).setHandled(true);
}
};
server = new Server(testPort);
server.setHandler(server1Handler);
server.start();
setupAndStartSSLServer(testPort);
Properties properties = new Properties();
addConnectionProperties(properties);
client = newBuilder(testPort, properties, createConnectionModule()).buildApi(IntegrationTestClient.class);
assert client != null;
assert client.newStringBuilder() != null;
}
private static void handlePost(HttpServletRequest request, HttpServletResponse response) throws IOException {
InputStream body = request.getInputStream();
try {
if (request.getHeader("Content-MD5") != null) {
String expectedMd5 = request.getHeader("Content-MD5");
String realMd5FromRequest;
realMd5FromRequest = base64().encode(asByteSource(body).hash(md5()).asBytes());
boolean matched = expectedMd5.equals(realMd5FromRequest);
if (matched) {
response.setStatus(SC_OK);
response.addHeader("x-Content-MD5", realMd5FromRequest);
} else {
response.sendError(500, "didn't match");
}
} else {
String responseString = (request.getContentLength() < 10240) ? toStringAndClose(body) + "POST" : "POST";
body = null;
for (String header : new String[] { CONTENT_DISPOSITION, CONTENT_LANGUAGE, CONTENT_ENCODING })
if (request.getHeader(header) != null) {
response.addHeader("x-" + header, request.getHeader(header));
}
response.setStatus(SC_OK);
response.getWriter().println(responseString);
}
Request.class.cast(request).setHandled(true);
} catch (IOException e) {
closeQuietly(body);
response.sendError(500, getStackTraceAsString(e));
}
}
protected void setupAndStartSSLServer(final int testPort) throws Exception {
Handler server2Handler = new AbstractHandler() {
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
if (request.getMethod().equals("PUT")) {
if (request.getContentLength() > 0) {
response.setStatus(SC_OK);
String text = toStringAndClose(request.getInputStream());
response.getWriter().println(text + "PUTREDIRECT");
}
} else if (request.getMethod().equals("POST")) {
if (request.getContentLength() > 0) {
handlePost(request, response);
} else {
handleAction(request, response);
}
} else if (request.getMethod().equals("HEAD")) {
// by HTML specification, HEAD response MUST NOT include a body
response.setContentType("text/xml");
response.setStatus(SC_OK);
} else {
response.setContentType("text/xml");
response.setStatus(SC_OK);
response.getWriter().println(XML2);
}
Request.class.cast(request).setHandled(true);
}
};
server2 = new Server();
server2.setHandler(server2Handler);
SslSelectChannelConnector ssl_connector = new SslSelectChannelConnector();
ssl_connector.setPort(testPort + 1);
ssl_connector.setMaxIdleTime(30000);
SslContextFactory ssl = ssl_connector.getSslContextFactory();
ssl.setKeyStorePath("src/test/resources/test.jks");
ssl.setKeyStorePassword("jclouds");
ssl.setTrustStore("src/test/resources/test.jks");
ssl.setTrustStorePassword("jclouds");
server2.setConnectors(new Connector[] { ssl_connector });
server2.start();
}
@SuppressWarnings("unchecked")
public static InputSupplier<InputStream> getTestDataSupplier() throws IOException {
byte[] oneConstitution = toByteArray(new GZIPInputStream(BaseJettyTest.class.getResourceAsStream("/const.txt.gz")));
InputSupplier<ByteArrayInputStream> constitutionSupplier = newInputStreamSupplier(oneConstitution);
InputSupplier<InputStream> temp = join(constitutionSupplier);
for (int i = 0; i < 100; i++) {
temp = join(temp, constitutionSupplier);
}
return temp;
}
public static ContextBuilder newBuilder(int testPort, Properties properties, Module... connectionModules) {
properties.setProperty(PROPERTY_TRUST_ALL_CERTS, "true");
properties.setProperty(PROPERTY_RELAX_HOSTNAME, "true");
return ContextBuilder
.newBuilder(
AnonymousProviderMetadata.forClientMappedToAsyncClientOnEndpoint(IntegrationTestClient.class,
IntegrationTestAsyncClient.class, "http://localhost:" + testPort))
.modules(ImmutableSet.<Module> copyOf(connectionModules)).overrides(properties);
}
@AfterClass
public void tearDownJetty() throws Exception {
closeQuietly(client);
if (server2 != null)
server2.stop();
server.stop();
}
protected abstract void addConnectionProperties(Properties props);
protected abstract Module createConnectionModule();
/**
* Fails every 10 requests.
*
* @param request
* @param response
* @return
* @throws IOException
*/
protected boolean failEveryTenRequests(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (cycle.incrementAndGet() % 10 == 0) {
response.sendError(500, "unlucky 10");
Request.class.cast(request).setHandled(true);
return true;
}
return false;
}
protected boolean redirectEveryTwentyRequests(HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (cycle.incrementAndGet() % 20 == 0) {
response.sendRedirect("http://localhost:" + (testPort + 1) + "/");
Request.class.cast(request).setHandled(true);
return true;
}
return false;
}
protected boolean failIfNoContentLength(HttpServletRequest request, HttpServletResponse response) throws IOException {
Multimap<String, String> realHeaders = LinkedHashMultimap.create();
Enumeration<String> headers = request.getHeaderNames();
while (headers.hasMoreElements()) {
String header = headers.nextElement().toString();
Enumeration<String> values = request.getHeaders(header);
while (values.hasMoreElements()) {
realHeaders.put(header, values.nextElement().toString());
}
}
if (realHeaders.get(CONTENT_LENGTH) == null) {
response.getWriter().println("no content length!");
response.getWriter().println(realHeaders.toString());
response.sendError(500, "no content length!");
Request.class.cast(request).setHandled(true);
return true;
}
return false;
}
private void handleAction(HttpServletRequest request, HttpServletResponse response) throws IOException {
final Matcher matcher = actionPattern.matcher(request.getRequestURI());
boolean matchFound = matcher.find();
if (matchFound) {
String objectId = matcher.group(1);
String action = matcher.group(2);
Builder<String, String> options = ImmutableMap.<String, String> builder();
if (matcher.groupCount() == 3) {
options.putAll(Splitter.on(';').withKeyValueSeparator("=").split(matcher.group(3)));
}
response.setStatus(SC_OK);
response.getWriter().println(objectId + "->" + action + ":" + options.build());
} else {
response.sendError(500, "no content");
}
}
}