Skip to content

Commit

Permalink
Added HTTP authentication to HTTPServer
Browse files Browse the repository at this point in the history
Signed-off-by: Doug Hoard <doug.hoard@gmail.com>
  • Loading branch information
dhoard committed Aug 13, 2021
1 parent 17c98eb commit 94cd5f3
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 13 deletions.
Expand Up @@ -299,7 +299,7 @@ private void assertExemplar(Histogram histogram, double value, String... labels)
}
if (lowerBound < value && value <= upperBound) {
Assert.assertNotNull("No exemplar found in bucket [" + lowerBound + ", " + upperBound + "]", bucket.exemplar);
Assert.assertEquals(value, bucket.exemplar.getValue(), 0.001);
Assert.assertEquals(value, bucket.exemplar.getValue(), 0.006);
Assert.assertEquals(labels.length/2, bucket.exemplar.getNumberOfLabels());
for (int i=0; i<labels.length; i+=2) {
Assert.assertEquals(labels[i], bucket.exemplar.getLabelName(i/2));
Expand Down
6 changes: 6 additions & 0 deletions simpleclient_httpserver/pom.xml
Expand Up @@ -57,5 +57,11 @@
<version>2.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -1,5 +1,6 @@
package io.prometheus.client.exporter;

import com.sun.net.httpserver.*;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.common.TextFormat;

Expand All @@ -21,10 +22,6 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPOutputStream;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

/**
* Expose Prometheus metrics using a plain Java HttpServer.
* <p>
Expand Down Expand Up @@ -166,42 +163,75 @@ static ThreadFactory defaultThreadFactory(boolean daemon) {
protected final ExecutorService executorService;

/**
* Start a HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}.
* Start a HTTP server serving Prometheus metrics from the given registry using the given {@link Authenticator}
* and {@link HttpServer}.
* The {@code httpServer} is expected to already be bound to an address
*/
public HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon) throws IOException {
public HTTPServer(HttpServer httpServer, Authenticator authenticator, CollectorRegistry registry, boolean daemon) throws IOException {
if (httpServer.getAddress() == null)
throw new IllegalArgumentException("HttpServer hasn't been bound to an address");

server = httpServer;
HttpHandler mHandler = new HTTPMetricHandler(registry);
server.createContext("/", mHandler);
server.createContext("/metrics", mHandler);
server.createContext("/-/healthy", mHandler);
HttpContext httpContext = server.createContext("/", mHandler);
if (authenticator != null) {
httpContext.setAuthenticator(authenticator);
}
httpContext = server.createContext("/metrics", mHandler);
if (authenticator != null) {
httpContext.setAuthenticator(authenticator);
}
httpContext = server.createContext("/-/healthy", mHandler);
if (authenticator != null) {
httpContext.setAuthenticator(authenticator);
}
executorService = Executors.newFixedThreadPool(5, NamedDaemonThreadFactory.defaultThreadFactory(daemon));
server.setExecutor(executorService);
start(daemon);
}

/**
* Start a HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}.
* The {@code httpServer} is expected to already be bound to an address
*/
public HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon) throws IOException {
this(httpServer, null, registry, daemon);
}

/**
* Start a HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}.
* The {@code httpServer} is expected to already be bound to an address
*/
public HTTPServer(HttpServer httpServer, CollectorRegistry registry) throws IOException {
this(httpServer, null, registry, false);
}

/**
* Start a HTTP server serving Prometheus metrics from the given registry.
*/
public HTTPServer(InetSocketAddress addr, Authenticator authenticator, CollectorRegistry registry, boolean daemon) throws IOException {
this(HttpServer.create(addr, 3), authenticator, registry, daemon);
}

/**
* Start a HTTP server serving Prometheus metrics from the given registry.
*/
public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException {
this(HttpServer.create(addr, 3), registry, daemon);
this(HttpServer.create(addr, 3), null, registry, daemon);
}

/**
* Start a HTTP server serving Prometheus metrics from the given registry using non-daemon threads.
*/
public HTTPServer(InetSocketAddress addr, CollectorRegistry registry) throws IOException {
this(addr, registry, false);
this(addr, null, registry, false);
}

/**
* Start a HTTP server serving the default Prometheus registry.
*/
public HTTPServer(int port, boolean daemon) throws IOException {
this(new InetSocketAddress(port), CollectorRegistry.defaultRegistry, daemon);
this(new InetSocketAddress(port), null, CollectorRegistry.defaultRegistry, daemon);
}

/**
Expand Down
@@ -0,0 +1,207 @@
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.CollectorRegistry;
import io.prometheus.client.Gauge;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import javax.net.ssl.SSLContext;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLConnection;
import java.util.Scanner;
import java.util.zip.GZIPInputStream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

public class TestHTTPServerBasicAuthentication {

private static final String HTTP_USER = "prometheus";
private static final String HTTP_PASSWORD = "some_password";

HTTPServer s;

@Before
public void init() throws IOException {
CollectorRegistry registry = new CollectorRegistry();
Gauge.build("a", "a help").register(registry);
Gauge.build("b", "a help").register(registry);
Gauge.build("c", "a help").register(registry);

Authenticator authenticator = new BasicAuthenticator("/") {
@Override
public boolean checkCredentials(String user, String password) {
return HTTP_USER.equals(user) && HTTP_PASSWORD.equals(password);
}
};

SSLContext sslContext = null;

HttpServer httpServer = HttpServer.create(new InetSocketAddress(0), 3);
s = new HTTPServer(httpServer, authenticator, registry, false);
}

@After
public void cleanup() {
s.stop();
}

String request(String context, String suffix) throws IOException {
return request(context, suffix, HTTP_USER, HTTP_PASSWORD);
}

String request(String context, String suffix, String user, String password) throws IOException {
String url = "http://localhost:" + s.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 request(String suffix) throws IOException {
return request("/metrics", suffix);
}

String requestWithCompression(String suffix) throws IOException {
return requestWithCompression("/metrics", suffix);
}

String requestWithCompression(String context, String suffix) throws IOException {
return requestWithCompression(context, suffix, HTTP_USER, HTTP_PASSWORD);
}

String requestWithCompression(String context, String suffix, String user, String password) throws IOException {
String url = "http://localhost:" + s.server.getAddress().getPort() + context + suffix;
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
if (user != null && password != null) {
connection.setRequestProperty("Authorization", encodeCredentials(user, password));
}
connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
connection.connect();
GZIPInputStream gzs = new GZIPInputStream(connection.getInputStream());
Scanner s = new Scanner(gzs).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}

String requestWithAccept(String accept) throws IOException {
return requestWithAccept(accept, HTTP_USER, HTTP_PASSWORD);
}

String requestWithAccept(String accept, String user, String password) throws IOException {
String url = "http://localhost:" + s.server.getAddress().getPort();
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
if (user != null && password != null) {
connection.setRequestProperty("Authorization", encodeCredentials(user, password));
}
connection.setRequestProperty("Accept", accept);
Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}

private static 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);
}
}

@Test(expected = IllegalArgumentException.class)
public void testRefuseUsingUnbound() throws IOException {
CollectorRegistry registry = new CollectorRegistry();
HTTPServer s = new HTTPServer(HttpServer.create(), registry, true);
s.stop();
}

@Test
public void testSimpleRequest() throws IOException {
String response = request("");
assertThat(response).contains("a 0.0");
assertThat(response).contains("b 0.0");
assertThat(response).contains("c 0.0");
}

@Test
public void testBadParams() throws IOException {
String response = request("?x");
assertThat(response).contains("a 0.0");
assertThat(response).contains("b 0.0");
assertThat(response).contains("c 0.0");
}

@Test
public void testSingleName() throws IOException {
String response = request("?name[]=a");
assertThat(response).contains("a 0.0");
assertThat(response).doesNotContain("b 0.0");
assertThat(response).doesNotContain("c 0.0");
}

@Test
public void testMultiName() throws IOException {
String response = request("?name[]=a&name[]=b");
assertThat(response).contains("a 0.0");
assertThat(response).contains("b 0.0");
assertThat(response).doesNotContain("c 0.0");
}

@Test
public void testDecoding() throws IOException {
String response = request("?n%61me[]=%61");
assertThat(response).contains("a 0.0");
assertThat(response).doesNotContain("b 0.0");
assertThat(response).doesNotContain("c 0.0");
}

@Test
public void testGzipCompression() throws IOException {
String response = requestWithCompression("");
assertThat(response).contains("a 0.0");
assertThat(response).contains("b 0.0");
assertThat(response).contains("c 0.0");
}

@Test
public void testOpenMetrics() throws IOException {
String response = requestWithAccept("application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1");
assertThat(response).contains("# EOF");
}

@Test
public void testHealth() throws IOException {
String response = request("/-/healthy", "");
assertThat(response).contains("Exporter is Healthy");
}

@Test
public void testHealthGzipCompression() throws IOException {
String response = requestWithCompression("/-/healthy", "");
assertThat(response).contains("Exporter is Healthy");
}

@Test(expected = IOException.class)
public void testHealthyBasicAuthenticationFailure() throws IOException {
String response = request("/-/healthy", "", null, null);
fail("Expected IOException");
}
}

0 comments on commit 94cd5f3

Please sign in to comment.