From ec6def41378f4a408f781a13669e2caf82aa6bf3 Mon Sep 17 00:00:00 2001 From: Doug Hoard Date: Sun, 19 Dec 2021 17:11:15 -0500 Subject: [PATCH] Changes to HTTPServer and HTTPMetricHandler to resolve getting Content-Length=0 when Transfer-Encoding=chunked. Refactored test classes for easier/cleaner testing. Signed-off-by: Doug Hoard --- .../client/exporter/HTTPServer.java | 4 +- .../client/exporter/HttpRequest.java | 244 ++++++++++ .../client/exporter/HttpResponse.java | 109 +++++ .../client/exporter/TestHTTPServer.java | 456 +++++++++--------- 4 files changed, 577 insertions(+), 236 deletions(-) create mode 100644 simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/HttpRequest.java create mode 100644 simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/HttpResponse.java diff --git a/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java b/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java index f56bc8860..834c32480 100644 --- a/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java +++ b/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java @@ -116,7 +116,9 @@ public void handle(HttpExchange t) throws IOException { } } else { long contentLength = response.size(); - t.getResponseHeaders().set("Content-Length", String.valueOf(contentLength)); + if (contentLength > 0) { + t.getResponseHeaders().set("Content-Length", String.valueOf(contentLength)); + } if (t.getRequestMethod().equals("HEAD")) { contentLength = -1; } diff --git a/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/HttpRequest.java b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/HttpRequest.java new file mode 100644 index 000000000..5b7865de4 --- /dev/null +++ b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/HttpRequest.java @@ -0,0 +1,244 @@ +package io.prometheus.client.exporter; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.xml.bind.DatatypeConverter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.Set; + +/** + * Class to perform HTTP testing + */ +public class HttpRequest { + + enum METHOD { GET, HEAD } + + private final Configuration configuration; + + /** + * Constructor + * + * @param configuration configuration + */ + private HttpRequest(Configuration configuration) { + this.configuration = configuration; + } + + /** + * Method to execute an HTTP request + * + * @return HttpResponse + * @throws IOException + */ + public HttpResponse execute() throws IOException { + if (configuration.url.toLowerCase().startsWith("https://") && (configuration.trustManagers != null)) { + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, configuration.trustManagers, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + + if (configuration.hostnameVerifier != null) { + HttpsURLConnection.setDefaultHostnameVerifier(configuration.hostnameVerifier); + } + } + + URLConnection urlConnection = new URL(configuration.url).openConnection(); + ((HttpURLConnection) urlConnection).setRequestMethod(configuration.method.toString()); + + Set>> entries = configuration.headers.entrySet(); + for (Map.Entry> entry : entries) { + for (String value : entry.getValue()) { + urlConnection.addRequestProperty(entry.getKey(), value); + } + } + + urlConnection.setUseCaches(false); + urlConnection.setDoInput(true); + urlConnection.setDoOutput(true); + urlConnection.connect(); + + Scanner scanner = new Scanner(urlConnection.getInputStream(), "UTF-8").useDelimiter("\\A"); + + return new HttpResponse( + ((HttpURLConnection) urlConnection).getResponseCode(), + urlConnection.getHeaderFields(), + urlConnection.getContentLength(), scanner.hasNext() ? scanner.next() : ""); + } + + /** + * Class to build an HttpRequest + */ + static class Builder { + + private final Configuration configuration; + + /** + * Constructor + */ + public Builder() { + configuration = new Configuration(); + } + + /** + * Method to set the HTTP request method + * + * @param method + * @return Builder + */ + public Builder withMethod(METHOD method) { + configuration.method = method; + return this; + } + + /** + * Method to set the HTTP request URL + * + * @param url + * @return Builder + */ + public Builder withURL(String url) { + configuration.url = url; + return this; + } + + /** + * Method to add an HTTP request header + * + * @param name + * @param value + * @return Builder + */ + public Builder withHeader(String name, String value) { + configuration.addHeader(name, value); + return this; + } + + /** + * Method to set the HTTP request "Authorization" header + * + * @param username + * @param password + * @return Builder + */ + public Builder withAuthorization(String username, String password) { + configuration.setHeader("Authorization", encodeCredentials(username, password)); + return this; + } + + /** + * Method to set the HTTP request trust managers when using an SSL URL + * + * @param trustManagers + * @return Builder + */ + public Builder withTrustManagers(TrustManager[] trustManagers) { + configuration.trustManagers = trustManagers; + return this; + } + + /** + * Method to set the HTTP request hostname verifier when using an SSL URL + * + * @param hostnameVerifier + * @return Builder + */ + public Builder withHostnameVerifier(HostnameVerifier hostnameVerifier) { + configuration.hostnameVerifier = hostnameVerifier; + return this; + } + + /** + * Method to build the HttpRequest + * + * @return HttpRequest + */ + public HttpRequest build() { + return new HttpRequest(configuration); + } + } + + /** + * Class used for Builder configuration + */ + private static class Configuration { + + public METHOD method; + public String url; + public Map> headers; + public TrustManager[] trustManagers; + public HostnameVerifier hostnameVerifier; + + /** + * Constructor + */ + Configuration() { + method = METHOD.GET; + headers = new HashMap>(); + } + + /** + * Method to add (append) an HTTP request header + * + * @param name + * @param value + * @return Configuration + */ + void addHeader(String name, String value) { + name = name.toLowerCase(); + List values = headers.get(name); + if (values == null) { + values = new LinkedList(); + headers.put(name, values); + } + + values.add(value); + } + + /** + * Method to set (overwrite) an HTTP request header, removing all previous header values + * + * @param name + * @param value + * @return Configuration + */ + void setHeader(String name, String value) { + List values = new LinkedList(); + values.add(value); + headers.put(name, values); + } + } + + /** + * Method to encode "Authorization" credentials + * + * @param username + * @param password + * @return String + */ + private final static String encodeCredentials(String username, String password) { + // Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8, + try { + byte[] credentialsBytes = (username + ":" + password).getBytes("UTF-8"); + return "Basic " + DatatypeConverter.printBase64Binary(credentialsBytes); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/HttpResponse.java b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/HttpResponse.java new file mode 100644 index 000000000..9ad49be41 --- /dev/null +++ b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/HttpResponse.java @@ -0,0 +1,109 @@ +package io.prometheus.client.exporter; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Class to perform HTTP testing + */ +class HttpResponse { + + private int responseCode; + private Map> headers; + private long contentLength = -1; + private String body; + + /** + * Constructor + * + * @param responseCode + * @param headers + * @param contentLength + * @param body + */ + HttpResponse(int responseCode, Map> headers, long contentLength, String body) throws IOException { + this.responseCode = responseCode; + this.body = body; + this.contentLength = contentLength; + this.headers = new HashMap>(); + + Set>> headerSet = headers.entrySet(); + for (String header : headers.keySet()) { + if (header != null) { + List values = headers.get(header); + this.headers.put(header.toLowerCase(), values); + } + } + + if (getHeader("content-length") != null && getHeader("transfer-encoding") != null) { + throw new IOException("Invalid HTTP response, should only contain Connect-Length or Transfer-Encoding"); + } + } + + /** + * Method to get the HTTP response code + * + * @return int + */ + public int getResponseCode() { + return this.responseCode; + } + + /** + * Method to get a list of HTTP response headers values + * + * @param name + * @return List + */ + public List getHeaderList(String name) { + return headers.get(name.toLowerCase()); + } + + /** + * Method to get the first HTTP response header value + * + * @param name + * @return String + */ + public String getHeader(String name) { + String value = null; + + List valueList = getHeaderList(name); + if (valueList != null && (valueList.size() >= 0)) { + value = valueList.get(0); + } + + return value; + } + + /** + * Method to get the first HTTP response header value as a Long. + * Returns null of the header doesn't exist + * + * @param name + * @return Long + */ + public Long getHeaderAsLong(String name) { + String value = getHeader(name); + if (value != null) { + try { + return Long.valueOf(value); + } catch (Exception e) { + } + } + + return null; + } + + /** + * Method to get the HTTP response body + * + * @return String + */ + public String getBody() { + return body; + } +} diff --git a/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java index bb8f7fa13..cf7f6aef4 100644 --- a/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java +++ b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java @@ -5,30 +5,14 @@ import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsParameters; -import io.prometheus.client.Gauge; import io.prometheus.client.CollectorRegistry; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.InetSocketAddress; -import java.net.URL; -import java.net.URLConnection; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.cert.X509Certificate; -import java.util.Scanner;; -import java.util.zip.GZIPInputStream; - +import io.prometheus.client.Gauge; import io.prometheus.client.SampleNameFilter; import org.junit.Assert; import org.junit.Before; -import org.junit.Test;; +import org.junit.Test; import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -38,6 +22,14 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import javax.xml.bind.DatatypeConverter; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.X509Certificate; import static org.assertj.core.api.Java6Assertions.assertThat; @@ -66,45 +58,45 @@ public class TestHTTPServer { HTTPS_CONFIGURATOR = createHttpsConfigurator(SSL_CONTEXT); } - private final static TrustManager[] TRUST_MANAGERS = new TrustManager[]{ + /** + * TrustManager[] that trusts all certificates + */ + private final static TrustManager[] TRUST_ALL_CERTS_TRUST_MANAGERS = new TrustManager[]{ new X509TrustManager() { - public java.security.cert.X509Certificate[] getAcceptedIssuers() { + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } - public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { + public void checkClientTrusted(X509Certificate[] certs, String authType) { } - public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { + public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; - private final static HostnameVerifier HOSTNAME_VERIFIER = new HostnameVerifier() { + /** + * HostnameVerifier that accepts any hostname + */ + private final static HostnameVerifier TRUST_ALL_HOSTS_HOSTNAME_VERIFIER = new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }; - final static Authenticator createAuthenticator(String realm, final String validUsername, final String validPassword) { - return new BasicAuthenticator(realm) { - @Override - public boolean checkCredentials(String username, String password) { - return validUsername.equals(username) && validPassword.equals(password); - } - }; + HttpRequest.Builder createHttpRequestBuilder(HTTPServer httpServer, String urlPath) { + return new HttpRequest.Builder().withURL("http://localhost:" + httpServer.getPort() + urlPath); } - class Response { - - public long contentLength; - public String body; + HttpRequest.Builder createHttpRequestBuilderWithSSL(HTTPServer httpServer, String urlPath) { + return new HttpRequest.Builder().withURL("https://localhost:" + httpServer.getPort() + urlPath) + .withTrustManagers(TRUST_ALL_CERTS_TRUST_MANAGERS) + .withHostnameVerifier(TRUST_ALL_HOSTS_HOSTNAME_VERIFIER); + } - public Response(long contentLength, String body) { - this.contentLength = contentLength; - this.body = body; - } + HttpRequest.Builder createHttpRequestBuilder(HttpServer httpServer, String urlPath) { + return new HttpRequest.Builder().withURL("http://localhost:" + httpServer.getAddress().getPort() + urlPath); } @Before @@ -115,316 +107,267 @@ public void init() throws IOException { Gauge.build("c", "a help").register(registry); } - Response request(String requestMethod, HTTPServer s, String context, String suffix) throws IOException { - String url = "http://localhost:" + s.server.getAddress().getPort() + context + suffix; - URLConnection connection = new URL(url).openConnection(); - ((HttpURLConnection)connection).setRequestMethod(requestMethod); - connection.setDoOutput(true); - connection.connect(); - Scanner scanner = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A"); - return new Response(connection.getContentLength(), scanner.hasNext() ? scanner.next() : ""); - } - - Response request(HTTPServer s, String context, String suffix) throws IOException { - return request("GET", s, context, suffix); - } - - Response request(HTTPServer s, String suffix) throws IOException { - return request(s, "/metrics", suffix); - } - - Response requestWithCompression(HTTPServer s, String suffix) throws IOException { - return requestWithCompression(s, "/metrics", suffix); - } - - Response requestWithCompression(HTTPServer s, String context, String suffix) throws IOException { - String url = "http://localhost:" + s.server.getAddress().getPort() + context + suffix; - URLConnection connection = new URL(url).openConnection(); - connection.setDoOutput(true); - connection.setDoInput(true); - connection.setRequestProperty("Accept-Encoding", "gzip, deflate"); - connection.connect(); - GZIPInputStream gzs = new GZIPInputStream(connection.getInputStream()); - Scanner scanner = new Scanner(gzs).useDelimiter("\\A"); - return new Response(connection.getContentLength(), scanner.hasNext() ? scanner.next() : ""); - } - - Response requestWithAccept(HTTPServer s, String accept) throws IOException { - String url = "http://localhost:" + s.server.getAddress().getPort(); - URLConnection connection = new URL(url).openConnection(); - connection.setDoOutput(true); - connection.setDoInput(true); - connection.setRequestProperty("Accept", accept); - Scanner scanner = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A"); - return new Response(connection.getContentLength(), scanner.hasNext() ? scanner.next() : ""); - } - - Response requestWithCredentials(HTTPServer httpServer, String context, String suffix, String username, String password) throws IOException { - String url = "http://localhost:" + httpServer.server.getAddress().getPort() + context + suffix; - URLConnection connection = new URL(url).openConnection(); - connection.setDoOutput(true); - if (username != null && password != null) { - connection.setRequestProperty("Authorization", encodeCredentials(username, password)); - } - connection.connect(); - Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A"); - return new Response(connection.getContentLength(), s.hasNext() ? s.next() : ""); - } - - Response requestWithSSL(String requestMethod, String username, String password, HTTPServer s, String context, String suffix) throws GeneralSecurityException, IOException { - String url = "https://localhost:" + s.server.getAddress().getPort() + context + suffix; - - SSLContext sc = SSLContext.getInstance("SSL"); - sc.init(null, TRUST_MANAGERS, new java.security.SecureRandom()); - HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); - HttpsURLConnection.setDefaultHostnameVerifier(HOSTNAME_VERIFIER); - - URLConnection connection = new URL(url).openConnection(); - ((HttpURLConnection)connection).setRequestMethod(requestMethod); - - if (username != null && password != null) { - connection.setRequestProperty("Authorization", encodeCredentials(username, password)); - } - - connection.setDoOutput(true); - connection.connect(); - Scanner scanner = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A"); - return new Response(connection.getContentLength(), scanner.hasNext() ? scanner.next() : ""); - } - - Response request(HttpServer httpServer, String context, String suffix) throws IOException { - String url = "http://localhost:" + httpServer.getAddress().getPort() + context + suffix; - URLConnection connection = new URL(url).openConnection(); - connection.setDoOutput(true); - connection.connect(); - Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A"); - return new Response(connection.getContentLength(), s.hasNext() ? s.next() : ""); - } - - String encodeCredentials(String username, String password) { - // Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8, - try { - byte[] credentialsBytes = (username + ":" + password).getBytes("UTF-8"); - return "Basic " + DatatypeConverter.printBase64Binary(credentialsBytes); - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException(e); - } - } - @Test(expected = IllegalArgumentException.class) public void testRefuseUsingUnbound() throws IOException { CollectorRegistry registry = new CollectorRegistry(); - HTTPServer s = new HTTPServer(HttpServer.create(), registry, true); - s.close(); + HTTPServer httpServer = new HTTPServer(HttpServer.create(), registry, true); + httpServer.close(); } @Test public void testSimpleRequest() throws IOException { - HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + HTTPServer httpServer = new HTTPServer(new InetSocketAddress(0), registry); + try { - String response = request(s, "").body; - assertThat(response).contains("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).contains("c 0.0"); + String body = createHttpRequestBuilder(httpServer, "/metrics").build().execute().getBody(); + assertThat(body).contains("a 0.0"); + assertThat(body).contains("b 0.0"); + assertThat(body).contains("c 0.0"); } finally { - s.close(); + httpServer.close(); } } @Test public void testBadParams() throws IOException { - HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + HTTPServer httpServer = new HTTPServer(new InetSocketAddress(0), registry); + try { - String response = request(s, "?x").body; - assertThat(response).contains("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).contains("c 0.0"); + String body = createHttpRequestBuilder(httpServer, "/metrics?x").build().execute().getBody(); + assertThat(body).contains("a 0.0"); + assertThat(body).contains("b 0.0"); + assertThat(body).contains("c 0.0"); } finally { - s.close(); + httpServer.close(); } } @Test public void testSingleName() throws IOException { - HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + HTTPServer httpServer = new HTTPServer(new InetSocketAddress(0), registry); + try { - String response = request(s, "?name[]=a").body; - assertThat(response).contains("a 0.0"); - assertThat(response).doesNotContain("b 0.0"); - assertThat(response).doesNotContain("c 0.0"); + String body = createHttpRequestBuilder(httpServer, "/metrics?name[]=a").build().execute().getBody(); + assertThat(body).contains("a 0.0"); + assertThat(body).doesNotContain("b 0.0"); + assertThat(body).doesNotContain("c 0.0"); } finally { - s.close(); + httpServer.close(); } } @Test public void testMultiName() throws IOException { - HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + HTTPServer httpServer = new HTTPServer(new InetSocketAddress(0), registry); + try { - String response = request(s, "?name[]=a&name[]=b").body; - assertThat(response).contains("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).doesNotContain("c 0.0"); + String body = createHttpRequestBuilder(httpServer, "/metrics?name[]=a&name[]=b").build().execute().getBody(); + assertThat(body).contains("a 0.0"); + assertThat(body).contains("b 0.0"); + assertThat(body).doesNotContain("c 0.0"); } finally { - s.close(); + httpServer.close(); } } @Test public void testSampleNameFilter() throws IOException { - HTTPServer s = new HTTPServer.Builder() + HTTPServer httpServer = new HTTPServer.Builder() + .withRegistry(registry) + .withSampleNameFilter(new SampleNameFilter.Builder() + .nameMustNotStartWith("a") + .build()) + .build(); + + try { + String body = createHttpRequestBuilder(httpServer, "/metrics?name[]=a&name[]=b").build().execute().getBody(); + assertThat(body).doesNotContain("a 0.0"); + assertThat(body).contains("b 0.0"); + assertThat(body).doesNotContain("c 0.0"); + } finally { + httpServer.close(); + } + } + + @Test + public void testSampleNameFilterEmptyBody() throws IOException { + HTTPServer httpServer = new HTTPServer.Builder() .withRegistry(registry) .withSampleNameFilter(new SampleNameFilter.Builder() .nameMustNotStartWith("a") + .nameMustNotStartWith("b") .build()) .build(); + try { - String response = request(s, "?name[]=a&name[]=b").body; - assertThat(response).doesNotContain("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).doesNotContain("c 0.0"); + HttpResponse httpResponse = createHttpRequestBuilder(httpServer, "/metrics?name[]=a&name[]=b").build().execute(); + assertThat(httpResponse.getBody()).isEmpty(); } finally { - s.close(); + httpServer.close(); } } @Test public void testDecoding() throws IOException { - HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + HTTPServer httpServer = new HTTPServer(new InetSocketAddress(0), registry); + try { - String response = request(s, "?n%61me[]=%61").body; - assertThat(response).contains("a 0.0"); - assertThat(response).doesNotContain("b 0.0"); - assertThat(response).doesNotContain("c 0.0"); + String body = createHttpRequestBuilder(httpServer, "/metrics?n%61me[]=%61").build().execute().getBody(); + assertThat(body).contains("a 0.0"); + assertThat(body).doesNotContain("b 0.0"); + assertThat(body).doesNotContain("c 0.0"); } finally { - s.close(); + httpServer.close(); } } @Test public void testGzipCompression() throws IOException { - HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + HTTPServer httpServer = new HTTPServer(new InetSocketAddress(0), registry); + try { - String response = requestWithCompression(s, "").body; - assertThat(response).contains("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).contains("c 0.0"); + String body = createHttpRequestBuilder(httpServer, "/metrics") + .withHeader("Accept", "gzip") + .withHeader("Accept", "deflate") + .build().execute().getBody(); + assertThat(body).contains("a 0.0"); + assertThat(body).contains("b 0.0"); + assertThat(body).contains("c 0.0"); } finally { - s.close(); + httpServer.close(); } } @Test public void testOpenMetrics() throws IOException { - HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + HTTPServer httpServer = new HTTPServer(new InetSocketAddress(0), registry); + try { - String response = requestWithAccept(s, "application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1").body; - assertThat(response).contains("# EOF"); + String body = createHttpRequestBuilder(httpServer, "/metrics") + .withHeader("Accept", "application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1") + .build().execute().getBody(); + assertThat(body).contains("# EOF"); } finally { - s.close(); + httpServer.close(); } } @Test public void testHealth() throws IOException { - HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + HTTPServer httpServer = new HTTPServer(new InetSocketAddress(0), registry); + try { - String response = request(s, "/-/healthy", "").body; - assertThat(response).contains("Exporter is Healthy"); + String body = createHttpRequestBuilder(httpServer, "/-/healthy").build().execute().getBody(); + assertThat(body).contains("Exporter is Healthy"); } finally { - s.close(); + httpServer.close(); } } @Test public void testHealthGzipCompression() throws IOException { - HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + HTTPServer httpServer = new HTTPServer(new InetSocketAddress(0), registry); + try { - String response = requestWithCompression(s, "/-/healthy", "").body; - assertThat(response).contains("Exporter is Healthy"); + String body = createHttpRequestBuilder(httpServer, "/-/healthy") + .withHeader("Accept", "gzip") + .withHeader("Accept", "deflate") + .build().execute().getBody(); + assertThat(body).contains("Exporter is Healthy"); } finally { - s.close(); + httpServer.close(); } } @Test public void testBasicAuthSuccess() throws IOException { - HTTPServer s = new HTTPServer.Builder() + HTTPServer httpServer = new HTTPServer.Builder() .withRegistry(registry) .withAuthenticator(createAuthenticator("/", "user", "secret")) .build(); + try { - String response = requestWithCredentials(s, "/metrics","?name[]=a&name[]=b", "user", "secret").body; - assertThat(response).contains("a 0.0"); + String body = createHttpRequestBuilder(httpServer, "/metrics?name[]=a&name[]=b") + .withAuthorization("user", "secret") + .build().execute().getBody(); + assertThat(body).contains("a 0.0"); } finally { - s.close(); + httpServer.close(); } } @Test public void testBasicAuthCredentialsMissing() throws IOException { - HTTPServer s = new HTTPServer.Builder() + HTTPServer httpServer = new HTTPServer.Builder() .withRegistry(registry) .withAuthenticator(createAuthenticator("/", "user", "secret")) .build(); + try { - request(s, "/metrics", "?name[]=a&name[]=b"); + createHttpRequestBuilder(httpServer, "/metrics?name[]=a&name[]=b").build().execute().getBody(); Assert.fail("expected IOException with HTTP 401"); } catch (IOException e) { Assert.assertTrue(e.getMessage().contains("401")); } finally { - s.close(); + httpServer.close(); } } @Test public void testBasicAuthWrongCredentials() throws IOException { - HTTPServer s = new HTTPServer.Builder() + HTTPServer httpServer = new HTTPServer.Builder() .withRegistry(registry) - .withAuthenticator(createAuthenticator("/", "user", "wrong")) + .withAuthenticator(createAuthenticator("/", "user", "secret")) .build(); + try { - request(s, "/metrics", "?name[]=a&name[]=b"); + createHttpRequestBuilder(httpServer, "/metrics?name[]=a&name[]=b") + .withAuthorization("user", "wrong") + .build().execute().getBody(); Assert.fail("expected IOException with HTTP 401"); } catch (IOException e) { Assert.assertTrue(e.getMessage().contains("401")); } finally { - s.close(); + httpServer.close(); } } @Test public void testHEADRequest() throws IOException { - HTTPServer s = new HTTPServer.Builder() + HTTPServer httpServer = new HTTPServer.Builder() .withRegistry(registry) .build(); - try { - Response response = request("HEAD", s, "/metrics", "?name[]=a&name[]=b"); - Assert.assertNotNull(response); - Assert.assertTrue(response.contentLength == 74); - Assert.assertTrue("".equals(response.body)); + try { + HttpResponse httpResponse = createHttpRequestBuilder(httpServer, "/metrics?name[]=a&name[]=b") + .withMethod(HttpRequest.METHOD.HEAD) + .build().execute(); + Assert.assertNotNull(httpResponse); + Assert.assertNotNull(httpResponse.getHeaderAsLong("content-length")); + Assert.assertTrue(httpResponse.getHeaderAsLong("content-length") == 74); + assertThat(httpResponse.getBody()).isEmpty(); } finally { - s.close(); + httpServer.close(); } } @Test public void testHEADRequestWithSSL() throws GeneralSecurityException, IOException { - HTTPServer s = new HTTPServer.Builder() + HTTPServer httpServer = new HTTPServer.Builder() .withRegistry(registry) .withHttpsConfigurator(HTTPS_CONFIGURATOR) .build(); try { - Response response = requestWithSSL( - "HEAD", null, null, s, "/metrics", "?name[]=a&name[]=b"); - - Assert.assertNotNull(response); - Assert.assertTrue(response.contentLength == 74); - Assert.assertTrue("".equals(response.body)); + HttpResponse httpResponse = createHttpRequestBuilderWithSSL(httpServer, "/metrics?name[]=a&name[]=b") + .withMethod(HttpRequest.METHOD.HEAD) + .build().execute(); + Assert.assertNotNull(httpResponse); + Assert.assertNotNull(httpResponse.getHeaderAsLong("content-length")); + Assert.assertTrue(httpResponse.getHeaderAsLong("content-length") == 74); + assertThat(httpResponse.getBody()).isEmpty(); } finally { - s.close(); + httpServer.close(); } } @@ -436,10 +379,10 @@ public void testSimpleRequestHttpServerWithHTTPMetricHandler() throws IOExceptio httpServer.start(); try { - String response = request(httpServer, "/metrics", null).body; - assertThat(response).contains("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).contains("c 0.0"); + String body = createHttpRequestBuilder(httpServer, "/metrics").build().execute().getBody(); + assertThat(body).contains("a 0.0"); + assertThat(body).contains("b 0.0"); + assertThat(body).contains("c 0.0"); } finally { httpServer.stop(0); } @@ -447,59 +390,84 @@ public void testSimpleRequestHttpServerWithHTTPMetricHandler() throws IOExceptio @Test public void testHEADRequestWithSSLAndBasicAuthSuccess() throws GeneralSecurityException, IOException { - HTTPServer s = new HTTPServer.Builder() + HTTPServer httpServer = new HTTPServer.Builder() .withRegistry(registry) .withHttpsConfigurator(HTTPS_CONFIGURATOR) .withAuthenticator(createAuthenticator("/", "user", "secret")) .build(); try { - Response response = requestWithSSL( - "HEAD", "user", "secret", s, "/metrics", "?name[]=a&name[]=b"); - - Assert.assertNotNull(response); - Assert.assertTrue(response.contentLength == 74); - Assert.assertTrue("".equals(response.body)); + HttpResponse httpResponse = createHttpRequestBuilderWithSSL(httpServer, "/metrics?name[]=a&name[]=b") + .withMethod(HttpRequest.METHOD.HEAD) + .withAuthorization("user", "secret") + .build().execute(); + Assert.assertNotNull(httpResponse); + Assert.assertNotNull(httpResponse.getHeaderAsLong("content-length")); + Assert.assertTrue(httpResponse.getHeaderAsLong("content-length") == 74); + assertThat(httpResponse.getBody()).isEmpty(); } finally { - s.close(); + httpServer.close(); } } @Test public void testHEADRequestWithSSLAndBasicAuthCredentialsMissing() throws GeneralSecurityException, IOException { - HTTPServer s = new HTTPServer.Builder() + HTTPServer httpServer = new HTTPServer.Builder() .withRegistry(registry) .withHttpsConfigurator(HTTPS_CONFIGURATOR) .withAuthenticator(createAuthenticator("/", "user", "secret")) .build(); try { - Response response = requestWithSSL("HEAD", null, null, s, "/metrics", "?name[]=a&name[]=b"); + createHttpRequestBuilderWithSSL(httpServer, "/metrics?name[]=a&name[]=b") + .withMethod(HttpRequest.METHOD.HEAD) + .build().execute(); Assert.fail("expected IOException with HTTP 401"); } catch (IOException e) { Assert.assertTrue(e.getMessage().contains("401")); } finally { - s.close(); + httpServer.close(); } } @Test public void testHEADRequestWithSSLAndBasicAuthWrongCredentials() throws GeneralSecurityException, IOException { - HTTPServer s = new HTTPServer.Builder() + HTTPServer httpServer = new HTTPServer.Builder() .withRegistry(registry) .withHttpsConfigurator(HTTPS_CONFIGURATOR) .withAuthenticator(createAuthenticator("/", "user", "secret")) .build(); try { - Response response = requestWithSSL("HEAD", "user", "wrong", s, "/metrics", "?name[]=a&name[]=b"); + createHttpRequestBuilderWithSSL(httpServer, "/metrics?name[]=a&name[]=b") + .withMethod(HttpRequest.METHOD.HEAD) + .withAuthorization("user", "wrong") + .build().execute(); Assert.fail("expected IOException with HTTP 401"); } catch (IOException e) { Assert.assertTrue(e.getMessage().contains("401")); } finally { - s.close(); + httpServer.close(); } } + + /** + * Encodes authorization credentials + * + * @param username + * @param password + * @return String + */ + private final static String encodeCredentials(String username, String password) { + // Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8, + try { + byte[] credentialsBytes = (username + ":" + password).getBytes("UTF-8"); + return "Basic " + DatatypeConverter.printBase64Binary(credentialsBytes); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + } + /** * Create an SSLContext * @@ -511,7 +479,7 @@ public void testHEADRequestWithSSLAndBasicAuthWrongCredentials() throws GeneralS * @throws GeneralSecurityException * @throws IOException */ - public static SSLContext createSSLContext(String sslContextType, String keyStoreType, String keyStorePath, String keyStorePassword) + private final static SSLContext createSSLContext(String sslContextType, String keyStoreType, String keyStorePath, String keyStorePassword) throws GeneralSecurityException, IOException { SSLContext sslContext = null; FileInputStream fileInputStream = null; @@ -550,7 +518,25 @@ public static SSLContext createSSLContext(String sslContextType, String keyStore } /** + * Creates an Authenticator * + * @param realm + * @param validUsername + * @param validPassword + * @return Authenticator + */ + private final static Authenticator createAuthenticator(String realm, final String validUsername, final String validPassword) { + return new BasicAuthenticator(realm) { + @Override + public boolean checkCredentials(String username, String password) { + return validUsername.equals(username) && validPassword.equals(password); + } + }; + } + + /** + * Creates an HttpsConfiguration + * * @param sslContext * @return HttpsConfigurator */