package org.jboss.as.test.integration.web.handlers;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.net.URI;
import java.nio.file.Path;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.websocket.ContainerProvider;
import javax.websocket.WebSocketContainer;
import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;
import org.apache.http.util.EntityUtils;
import org.jboss.as.test.integration.web.websocket.AnnotatedClient;
import org.jboss.logging.Logger;
import org.junit.Assert;
/**
* Tests the use of Undertow request dumping handler. This is base class that implements particular test behaviour and controls
* what is tested.
*
* @author <a href="mailto:jstourac@redhat.com">Jan Stourac</a>
*/
public abstract class RequestDumpingHandlerTestImpl {
private static Logger log = Logger.getLogger(RequestDumpingHandlerTestImpl.class);
private final int TOTAL_DELAY = 3000;
private final int SLEEP_TIMEOUT = 200;
private final Path logFilePath;
// Expected value of the "contentType" header in response - default is "text/plain" but can be overridden by child class
protected String contentType = "text/plain";
// Expected value of the "status" header in response - default is "200" but can be overridden by child class
protected String status = "200";
// Expected value of the "scheme" header in request - default is "http" but can be overridden by child class
protected String scheme = "http";
/**
* Constructor that immediately executes test body.
*
* @param uri testing URI to connect to
* @param logFilePath path to log file in which are logged request dumps
* @param requestDumperOn whether request dumping feature is enabled at all
*/
public RequestDumpingHandlerTestImpl(URI uri, Path logFilePath, boolean requestDumperOn) throws Exception {
this.logFilePath = logFilePath;
commonTestBody(uri, requestDumperOn);
}
/***
* Abstract method which implements way of performing request to the server. It should be implemented depending on what type
* of request we want to perform (HTTP, HTTPS, etc.).
*
* @param uri testing URI to connect to
* @return 2dimensional array - request (as a first member) and response (as a second member) headers arrays; in case that
* there is no simple way how to obtain request and response headers then returns null
* @throws Exception
*/
public abstract Header[][] performRequest(URI uri) throws Exception;
/**
* Common test body part.
*
* @param uri deployment URI
* @param requestDumperOn whether RequestDumpingHandler is turned on
* @throws Exception
*/
private void commonTestBody(URI uri, boolean requestDumperOn) throws Exception {
// Test whether custom log file exists already. If so then count number of lines in it so further we will ignore them.
long skipBytes = 0;
if (logFilePath.toFile().exists() && logFilePath.toFile().isFile()) {
skipBytes = logFilePath.toFile().length();
} else {
log.trace("The log file ('" + logFilePath + "') does not exist yet, that is ok.");
}
// Perform request on server...
Header[] reqHdrs = null;
Header[] respHdrs = null;
Header[][] hdrs = performRequest(uri);
if (hdrs != null) {
reqHdrs = hdrs[0];
respHdrs = hdrs[1];
}
// Test whether there is request dump for particular URL after the HTTP request executed...
testLogForDumpWithURL(logFilePath, uri.getPath(), skipBytes, requestDumperOn);
if (requestDumperOn) {
// If we expect request dump -> check its data.
checkReqDumpData(logFilePath, skipBytes, reqHdrs, respHdrs, uri.getHost(), uri.getPort(), uri.getPath());
}
}
/**
* Reads content of the file into a string variable.
*
* @param logFilePath
* @param skipBytes number of bytes from the beginning of the file that should be skipped
* @return content of the file as a string
* @throws FileNotFoundException
*/
private String readLogFile(Path logFilePath, long skipBytes) throws FileNotFoundException, IOException {
Assert.assertTrue("Log file ('" + logFilePath + "') does not exist", logFilePath.toFile().exists());
Assert.assertTrue("The '" + logFilePath + "' is not a file", logFilePath.toFile().isFile());
// logfile exists -> read its content...
LineNumberReader lnr = new LineNumberReader(new FileReader(logFilePath.toFile()));
StringBuilder sb = new StringBuilder();
log.trace("I am skipping '" + skipBytes + "' bytes from the beggining of the file.");
lnr.skip(skipBytes);
String input;
while ((input = lnr.readLine()) != null) {
sb.append("\n" + input);
}
lnr.close();
return sb.toString();
}
/**
* Searching log for request dump of request to particular path. If no such request dump is found there is sanity loop to
* ensure that system has had enough time to write data to the disk.
*
* @param logFilePath path to log file
* @param path URI path searched for in log file
* @param skipBytes number of bytes from the beginning of the file that should be skipped
* @param expected whether we expect to find given path
* @throws FileNotFoundException
*/
private void testLogForDumpWithURL(Path logFilePath, String path, long skipBytes, boolean expected) throws Exception {
Pattern pattern = Pattern.compile("-+REQUEST-+.+" + path + ".+-+RESPONSE-+", Pattern.DOTALL);
Matcher m;
long startTime = System.currentTimeMillis();
boolean hasFound = false;
long currTime;
String content;
// Give system time to write data on disk...
do {
currTime = System.currentTimeMillis();
content = readLogFile(logFilePath, skipBytes);
m = pattern.matcher(content);
// Search for pattern...
if (m.find()) {
hasFound = true;
break;
}
Thread.sleep(SLEEP_TIMEOUT);
} while (currTime - startTime < TOTAL_DELAY);
log.trace("I have read following content of the file '" + logFilePath + "':\n" + content + "\n---END-OF-FILE-OUTPUT---");
// Finally compare search result with our expectation...
Assert.assertEquals("Searching for pattern: '" + pattern + "' in log file ('" + logFilePath.toString() + "')",
expected, hasFound);
}
/**
* Check request dumper data.
*
* @param logFilePath path to log file
* @param skipBytes number of bytes from the beginning of the file that should be skipped
* @param reqHdrs request headers
* @param respHdrs response headers
* @param host server IP address
* @param port server listening port
* @param path URI path
* @throws IOException
*/
private void checkReqDumpData(Path logFilePath, long skipBytes, Header[] reqHdrs, Header[] respHdrs, String host, int port, String path) throws IOException {
String content = readLogFile(logFilePath, skipBytes);
// Split into request and response part:
String request = content.substring(0, content.indexOf("-RESPONSE-"));
String response = content.substring(content.indexOf("-RESPONSE-"), content.length());
// Check request dump part...
searchInFile(request, "-+REQUEST-+");
searchInFile(request, "\\s+URI=" + Pattern.quote(path));
searchInFile(request, "\\s+characterEncoding=");
searchInFile(request, "\\s+contentLength=");
searchInFile(request, "\\s+contentType=");
searchForHeaders(request, reqHdrs);
searchInFile(request, "\\s+locale=\\[.*\\]");
searchInFile(request, "\\s+method=GET");
searchInFile(request, "\\s+protocol=");
searchInFile(request, "\\s+queryString=");
searchInFile(request, "\\s+remoteAddr=");
searchInFile(request, "\\s+remoteHost=");
searchInFile(request, "\\s+scheme=" + Pattern.quote(scheme));
searchInFile(request, "\\s+host=" + Pattern.quote(host));
searchInFile(request, "\\s+serverPort=" + Pattern.quote(String.valueOf(port)));
// Now check response dump part...
searchInFile(response, "-+RESPONSE-+");
searchInFile(response, "\\s+contentLength=");
searchInFile(response, "\\s+contentType=" + Pattern.quote(contentType));
searchForHeaders(response, respHdrs);
searchInFile(response, "\\s+status=" + Pattern.quote(status));
}
/**
* Search for request and response headers. Respect that they might be in different order.
*
* @param content content in which is searched
* @param hdrs array of headers which should be searched for; if null then no check will be performed; if zero length then
* no header line is expected in the log
* @throws FileNotFoundException
*/
private void searchForHeaders(String content, Header[] hdrs) throws FileNotFoundException {
if (hdrs == null) {
log.trace("No array with headers given -> skipping testing header content in log file.");
return;
}
final String HEADER_REGEXP = "\\s+header=";
if (hdrs.length > 0) {
// Check that number of "header" occurrences equals to hdrs.length
Assert.assertEquals("", hdrs.length, countMatch(content, HEADER_REGEXP));
for (Header hdr : hdrs) {
// Close current scanner, reopen it and move to start pattern directly...
searchInFile(content, HEADER_REGEXP + Pattern.quote(hdr.getName()) + "=" + Pattern.quote(hdr.getValue()));
}
} else {
// request contains no headers -> we really do not expect it to be in dump
searchInFile(content, HEADER_REGEXP, false);
}
}
/**
* Searches in given content for given pattern.
*
* @param content content of the file as a string
* @param regExp regular expression that is searched in the file
*/
private void searchInFile(String content, String regExp) {
searchInFile(content, regExp, true);
}
/**
* Searches in given content for given pattern.
*
* @param content content of the file as a string
* @param regExp regular expression that is searched in the file
* @param expected whether searching pattern is expected to be found or not
*/
private void searchInFile(String content, String regExp, boolean expected) {
Pattern pattern = Pattern.compile(regExp);
Matcher m = pattern.matcher(content);
Assert.assertEquals("Searching for pattern: '" + regExp + "' in log file ('" + logFilePath.toString() + "')", expected,
m.find());
}
/**
* Counts number of occurrences of given string in given content.
*
* @param content in this content will be searching for given pattern
* @param regExp given pattern to search in content
* @return number of occurrences in given content
*/
private int countMatch(String content, String regExp) {
Pattern pattern = Pattern.compile(regExp);
Matcher m = pattern.matcher(content);
int occurs = 0;
while (m.find()) {
occurs++;
}
return occurs;
}
/**
* Testing class which implements HTTPS requests on server.
*
* @author <a href="mailto:jstourac@redhat.com">Jan Stourac</a>
*/
static class HttpsRequestDumpingHandlerTestImpl extends RequestDumpingHandlerTestImpl {
HttpsRequestDumpingHandlerTestImpl(URI uri, Path logFilePath, boolean requestDumperOn) throws Exception {
super(uri, logFilePath, requestDumperOn);
}
@Override
public Header[][] performRequest(URI uri) throws Exception {
// Override value of the "scheme" header expected in response
scheme = "https";
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
}};
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection httpsConn = (HttpsURLConnection) uri.toURL().openConnection();
httpsConn.setDoOutput(false);
httpsConn.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
BufferedReader br = new BufferedReader(new InputStreamReader(httpsConn.getInputStream()));
StringBuilder sb = new StringBuilder();
String input;
while ((input = br.readLine()) != null) {
sb = sb.append(input);
}
br.close();
httpsConn.disconnect();
Header[][] reqAndrespHeaders = new Header[2][];
reqAndrespHeaders[1] = retrieveHeaders(httpsConn.getHeaderFields());
log.trace("The content of the URL ('" + uri + "'):\n" + sb.toString());
Assert.assertEquals(200, httpsConn.getResponseCode());
Assert.assertEquals("Could not reach expected content via http request", "A file", sb.toString());
// NOTE: leaving request headers null (won't be checked) as there is no easy way how to obtain them
return reqAndrespHeaders;
}
private Header[] retrieveHeaders(Map<String, List<String>> headers) {
//System.out.println("---- PRINTING HEADERS ----");
LinkedList<Header> hdrsList = new LinkedList<Header>();
for (String header : headers.keySet()) {
for (String value : headers.get(header)) {
if (header != null) {
//System.out.println(header + ": " + value);
hdrsList.add(new BasicHeader(header, value));
}
}
}
return hdrsList.toArray(new Header[hdrsList.size()]);
}
}
/**
* Testing class that implements standard HTTP requests.
*
* @author <a href="mailto:jstourac@redhat.com">Jan Stourac</a>
*/
static class HttpRequestDumpingHandlerTestImpl extends RequestDumpingHandlerTestImpl {
HttpRequestDumpingHandlerTestImpl(URI uri, Path logFilePath, boolean requestDumperOn) throws Exception {
super(uri, logFilePath, requestDumperOn);
}
@Override
public Header[][] performRequest(URI uri) throws Exception {
Header[][] ret = new Header[2][];
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
HttpGet httpget = new HttpGet(uri.toURL().toExternalForm() + "file.txt");
HttpContext localContext = new BasicHttpContext();
HttpResponse response = httpClient.execute(httpget, localContext);
HttpRequest request = (HttpRequest) localContext.getAttribute(HttpCoreContext.HTTP_REQUEST);
// Fill return values
ret[0] = request.getAllHeaders();
ret[1] = response.getAllHeaders();
StatusLine statusLine = response.getStatusLine();
Assert.assertEquals(200, statusLine.getStatusCode());
String result = EntityUtils.toString(response.getEntity());
Assert.assertEquals("Could not reach expected content via http request", "A file", result);
}
return ret;
}
}
/**
* Testing class that implements standard websocket requests via http upgrade.
*
* @author <a href="mailto:jstourac@redhat.com">Jan Stourac</a>
*/
static class WsRequestDumpingHandlerTestImpl extends RequestDumpingHandlerTestImpl {
WsRequestDumpingHandlerTestImpl(URI uri, Path logFilePath, boolean requestDumperOn) throws Exception {
super(uri, logFilePath, requestDumperOn);
}
@Override
public Header[][] performRequest(URI uri) throws Exception {
// Override value of the "contentType" header expected in response
contentType = "null";
// Override value of the "status" header expected in response
status = "101";
AnnotatedClient endpoint = new AnnotatedClient();
WebSocketContainer serverContainer = ContainerProvider.getWebSocketContainer();
serverContainer.connectToServer(endpoint, uri);
Assert.assertEquals("Hello Stuart", endpoint.getMessage());
// NOTE: leaving request and response headers null (won't be checked) as there is no easy way how to obtain them
return null;
}
}
}