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(); + } + } }