package com.github.kristofa.test.http;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.HashSet;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import org.simpleframework.http.core.Container;
import org.simpleframework.transport.connect.Connection;
import org.simpleframework.transport.connect.SocketConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.kristofa.test.http.client.ApacheHttpClientImpl;
import com.github.kristofa.test.http.client.HttpClient;
import com.github.kristofa.test.http.client.HttpClientResponse;
import com.github.kristofa.test.http.client.HttpRequestException;
/**
* Http proxy that supports logging requests/reponses. Its purpose is to be a 'man in the middle' which can be used to
* capture request/responses that can be mocked later on for testing purposes. It forwards requests it gets to another
* service and returns the result of that service unmodified to the requester. While doing that it also allows logging
* request/response pairs.
* <p>
* Those persisted request/response pairs can be mocked by {@link MockHttpServer}.
* <p>
* Using the {@link LoggingHttpProxy} to persist request/responses and using them with the {@link MockHttpServer} is
* especially useful for complex responses that are not that easy to mock by hand. It allows building a <a
* href="http://googletesting.blogspot.be/2012/10/hermetic-servers.html">hermetic server</a>.
* <p>
* The {@link LoggingHttpProxy} will return following HTTP return codes when things go wrong:
* <ul>
* <li>570: We could not build a forward request for input request. Missing of faulty {@link ForwardHttpRequestBuilder}.
* <li>571: Forward request failed. Forward URL invalid?
* <li>572: Copying response of forwarding request failed.
* <li>573: Unknown exception.
* </ul>
* The body of the response will contain the error message.
*
* @author kristof
*/
public class LoggingHttpProxy {
private final static Logger LOGGER = LoggerFactory.getLogger(LoggingHttpProxy.class);
private final int port;
private final Collection<ForwardHttpRequestBuilder> requestBuilders = new HashSet<ForwardHttpRequestBuilder>();
private final HttpRequestResponseLoggerFactory loggerFactory;
private Connection connection;
private ProxyImplementation proxy;
private class ProxyImplementation implements Container {
private static final int UNKNOWN_EXCEPTION_HTTP_CODE = 573;
private static final int FORWARD_REQUEST_FAILED_HTTP_CODE = 571;
private static final int COPY_RESPONSE_FAILED_ERROR_HTTP_CODE = 572;
private static final int NO_FORWARD_REQUEST_ERROR_HTTP_CODE = 570;
private static final String CONTENT_TYPE = "Content-Type";
public ProxyImplementation() {
super();
}
/**
* {@inheritDoc}
*/
@Override
public void handle(final Request request, final Response response) {
try {
final FullHttpRequest httpRequest = RequestConvertor.convert(request);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Received request: " + httpRequest);
}
FullHttpRequest forwardHttpRequest = null;
for (final ForwardHttpRequestBuilder forwardRequestBuilder : requestBuilders) {
forwardHttpRequest = forwardRequestBuilder.getForwardRequest(httpRequest);
if (forwardHttpRequest != null) {
break;
}
}
if (forwardHttpRequest == null) {
LOGGER.error("Got unexpected request: " + httpRequest);
errorResponse(response, NO_FORWARD_REQUEST_ERROR_HTTP_CODE, "Received unexpected request:\n"
+ httpRequest.toString());
} else {
LOGGER.debug("Logging request.");
final HttpRequestResponseLogger logger = loggerFactory.getHttpRequestResponseLogger();
logger.log(httpRequest);
try {
LOGGER.debug("Forward request.");
final HttpClientResponse<InputStream> forwardResponse = forward(forwardHttpRequest);
LOGGER.debug("Got response for forward request.");
try {
final InputStream inputStream = forwardResponse.getResponseEntity();
byte[] responseEntity;
try {
// This is tricky as we keep the full response in memory... reason is that we need to copy it
// twice.
// Once to return to response, another time to log.
responseEntity = IOUtils.toByteArray(inputStream);
} finally {
inputStream.close();
}
final HttpResponse httpResponse =
new HttpResponseImpl(forwardResponse.getHttpCode(), forwardResponse.getContentType(),
responseEntity);
LOGGER.debug("Logging response");
logger.log(httpResponse);
response.setCode(forwardResponse.getHttpCode());
response.set(CONTENT_TYPE, forwardResponse.getContentType());
final OutputStream outputStream = response.getOutputStream();
try {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(responseEntity);
IOUtils.copy(byteArrayInputStream, outputStream);
byteArrayInputStream.close();
} finally {
outputStream.close();
}
} catch (final IOException e) {
LOGGER.error("IOException when trying to copy response of forward request.", e);
errorResponse(response, COPY_RESPONSE_FAILED_ERROR_HTTP_CODE, "Exception when copying streams."
+ e.getMessage());
} finally {
forwardResponse.close();
}
} catch (final HttpRequestException e) {
LOGGER.error("HttpRequestException when forwarding request.", e);
errorResponse(response, FORWARD_REQUEST_FAILED_HTTP_CODE,
"Exception when forwarding request." + e.getMessage());
}
}
} catch (final Exception e) {
LOGGER.error("Exception.", e);
errorResponse(response, UNKNOWN_EXCEPTION_HTTP_CODE, "Exception: " + e.getMessage());
}
}
private HttpClientResponse<InputStream> forward(final FullHttpRequest request) throws HttpRequestException {
final HttpClient client = new ApacheHttpClientImpl();
return client.execute(request);
}
private void errorResponse(final Response response, final int httpCode, final String message) {
response.setCode(httpCode);
response.set(CONTENT_TYPE, "text/plain;charset=utf-8");
PrintStream body;
try {
body = response.getPrintStream();
body.print(message);
body.close();
} catch (final IOException e) {
throw new IllegalStateException("Exception when building response.", e);
}
}
}
/**
* Create a new instance.
*
* @param port Port at which proxy will be running.
* @param requestBuilders Forward request builders. Should not be <code>null</code> and at least 1 should be specified.
* @param loggerFactory Request/Response logger factory.. Should not be <code>null</code>.
*/
public LoggingHttpProxy(final int port, final Collection<ForwardHttpRequestBuilder> requestBuilders,
final HttpRequestResponseLoggerFactory loggerFactory) {
Validate.isTrue(requestBuilders != null && !requestBuilders.isEmpty(),
"At least 1 ForwardHttpRequestBuilder should be provided.");
Validate.notNull(loggerFactory, "HttpRequestResponseLoggerFactory should not be null.");
this.port = port;
this.requestBuilders.addAll(requestBuilders);
this.loggerFactory = loggerFactory;
}
/**
* Starts proxy.
*
* @throws Exception In case starting fails.
*/
public void start() throws IOException {
// Close existing connection if it exists.
if (connection != null) {
connection.close();
}
proxy = new ProxyImplementation();
connection = new SocketConnection(proxy);
final SocketAddress address = new InetSocketAddress(port);
connection.connect(address);
LOGGER.debug("Started on port: " + port);
}
/**
* Stops proxy.
*
* @throws IOException In case closing connection fails.
*/
public void stop() throws IOException {
LOGGER.debug("Stopping and closing connection.");
connection.close();
}
}