diff --git a/simpleclient_httpserver/pom.xml b/simpleclient_httpserver/pom.xml
index 0106b3ff7..475ab4fc4 100644
--- a/simpleclient_httpserver/pom.xml
+++ b/simpleclient_httpserver/pom.xml
@@ -56,5 +56,11 @@
2.6.0
test
+
+ javax.xml.bind
+ jaxb-api
+ 2.3.0
+ test
+
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 dbc031307..2086f572f 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
@@ -26,6 +26,8 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPOutputStream;
+import com.sun.net.httpserver.Authenticator;
+import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
@@ -195,6 +197,7 @@ public static class Builder {
private boolean daemon = false;
private Predicate sampleNameFilter;
private Supplier> sampleNameFilterSupplier;
+ private Authenticator authenticator;
/**
* Port to bind to. Must not be called together with {@link #withInetSocketAddress(InetSocketAddress)}
@@ -286,6 +289,18 @@ public Builder withRegistry(CollectorRegistry registry) {
return this;
}
+ /**
+ * Optional: {@link Authenticator} to use to support authentication.
+ */
+ public Builder withAuthenticator(Authenticator authenticator) {
+ this.authenticator = authenticator;
+ return this;
+ }
+
+ /**
+ * Build the HTTPServer
+ * @throws IOException
+ */
public HTTPServer build() throws IOException {
if (sampleNameFilter != null) {
assertNull(sampleNameFilterSupplier, "cannot configure 'sampleNameFilter' and 'sampleNameFilterSupplier' at the same time");
@@ -296,7 +311,7 @@ public HTTPServer build() throws IOException {
assertNull(hostname, "cannot configure 'httpServer' and 'hostname' at the same time");
assertNull(inetAddress, "cannot configure 'httpServer' and 'inetAddress' at the same time");
assertNull(inetSocketAddress, "cannot configure 'httpServer' and 'inetSocketAddress' at the same time");
- return new HTTPServer(httpServer, registry, daemon, sampleNameFilterSupplier);
+ return new HTTPServer(httpServer, registry, daemon, sampleNameFilterSupplier, authenticator);
} else if (inetSocketAddress != null) {
assertZero(port, "cannot configure 'inetSocketAddress' and 'port' at the same time");
assertNull(hostname, "cannot configure 'inetSocketAddress' and 'hostname' at the same time");
@@ -309,7 +324,7 @@ public HTTPServer build() throws IOException {
} else {
inetSocketAddress = new InetSocketAddress(port);
}
- return new HTTPServer(HttpServer.create(inetSocketAddress, 3), registry, daemon, sampleNameFilterSupplier);
+ return new HTTPServer(HttpServer.create(inetSocketAddress, 3), registry, daemon, sampleNameFilterSupplier, authenticator);
}
private void assertNull(Object o, String msg) {
@@ -330,7 +345,7 @@ private void assertZero(int i, String msg) {
* The {@code httpServer} is expected to already be bound to an address
*/
public HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon) throws IOException {
- this(httpServer, registry, daemon, null);
+ this(httpServer, registry, daemon, null, null);
}
/**
@@ -375,15 +390,24 @@ public HTTPServer(String host, int port) throws IOException {
this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, false);
}
- private HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon, Supplier> sampleNameFilterSupplier) {
+ private HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon, Supplier> sampleNameFilterSupplier, Authenticator authenticator) {
if (httpServer.getAddress() == null)
throw new IllegalArgumentException("HttpServer hasn't been bound to an address");
server = httpServer;
HttpHandler mHandler = new HTTPMetricHandler(registry, sampleNameFilterSupplier);
- server.createContext("/", mHandler);
- server.createContext("/metrics", mHandler);
- server.createContext("/-/healthy", mHandler);
+ HttpContext mContext = server.createContext("/", mHandler);
+ if (authenticator != null) {
+ mContext.setAuthenticator(authenticator);
+ }
+ mContext = server.createContext("/metrics", mHandler);
+ if (authenticator != null) {
+ mContext.setAuthenticator(authenticator);
+ }
+ mContext = server.createContext("/-/healthy", mHandler);
+ if (authenticator != null) {
+ mContext.setAuthenticator(authenticator);
+ }
executorService = Executors.newFixedThreadPool(5, NamedDaemonThreadFactory.defaultThreadFactory(daemon));
server.setExecutor(executorService);
start(daemon);
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 af99ccf1f..f0dfe874a 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
@@ -1,9 +1,12 @@
package io.prometheus.client.exporter;
+import com.sun.net.httpserver.Authenticator;
+import com.sun.net.httpserver.BasicAuthenticator;
import com.sun.net.httpserver.HttpServer;
import io.prometheus.client.Gauge;
import io.prometheus.client.CollectorRegistry;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLConnection;
@@ -11,9 +14,12 @@
import java.util.zip.GZIPInputStream;
import io.prometheus.client.SampleNameFilter;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
+import javax.xml.bind.DatatypeConverter;
+
import static org.assertj.core.api.Java6Assertions.assertThat;
public class TestHTTPServer {
@@ -67,6 +73,39 @@ String requestWithAccept(HTTPServer s, String accept) throws IOException {
return scanner.hasNext() ? scanner.next() : "";
}
+ String requestWithCredentials(HTTPServer httpServer, String context, String suffix, String user, String password) throws IOException {
+ String url = "http://localhost:" + httpServer.server.getAddress().getPort() + context + suffix;
+ URLConnection connection = new URL(url).openConnection();
+ connection.setDoOutput(true);
+ if (user != null && password != null) {
+ connection.setRequestProperty("Authorization", encodeCredentials(user, password));
+ }
+ connection.connect();
+ Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A");
+ return s.hasNext() ? s.next() : "";
+ }
+
+ String encodeCredentials(String user, String password) {
+ // Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8,
+ try {
+ byte[] credentialsBytes = (user + ":" + password).getBytes("UTF-8");
+ String encoded = DatatypeConverter.printBase64Binary(credentialsBytes);
+ encoded = String.format("Basic %s", encoded);
+ return encoded;
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ 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);
+ }
+ };
+ }
+
@Test(expected = IllegalArgumentException.class)
public void testRefuseUsingUnbound() throws IOException {
CollectorRegistry registry = new CollectorRegistry();
@@ -202,4 +241,50 @@ public void testHealthGzipCompression() throws IOException {
s.close();
}
}
+
+ @Test
+ public void testBasicAuthSuccess() throws IOException {
+ HTTPServer s = new HTTPServer.Builder()
+ .withRegistry(registry)
+ .withAuthenticator(createAuthenticator("/", "user", "secret"))
+ .build();
+ try {
+ String response = requestWithCredentials(s, "/metrics","?name[]=a&name[]=b", "user", "secret");
+ assertThat(response).contains("a 0.0");
+ } finally {
+ s.close();
+ }
+ }
+
+ @Test
+ public void testBasicAuthCredentialsMissing() throws IOException {
+ HTTPServer s = new HTTPServer.Builder()
+ .withRegistry(registry)
+ .withAuthenticator(createAuthenticator("/", "user", "secret"))
+ .build();
+ try {
+ request(s, "/metrics", "?name[]=a&name[]=b");
+ Assert.fail("expected IOException with HTTP 401");
+ } catch (IOException e) {
+ Assert.assertTrue(e.getMessage().contains("401"));
+ } finally {
+ s.close();
+ }
+ }
+
+ @Test
+ public void testBasicAuthWrongCredentials() throws IOException {
+ HTTPServer s = new HTTPServer.Builder()
+ .withRegistry(registry)
+ .withAuthenticator(createAuthenticator("/", "user", "wrong"))
+ .build();
+ try {
+ request(s, "/metrics", "?name[]=a&name[]=b");
+ Assert.fail("expected IOException with HTTP 401");
+ } catch (IOException e) {
+ Assert.assertTrue(e.getMessage().contains("401"));
+ } finally {
+ s.close();
+ }
+ }
}