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 29, 2021
1 parent c3306c4 commit c72efdc
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 7 deletions.
6 changes: 6 additions & 0 deletions simpleclient_httpserver/pom.xml
Expand Up @@ -56,5 +56,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>
Expand Up @@ -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;
Expand Down Expand Up @@ -195,6 +197,7 @@ public static class Builder {
private boolean daemon = false;
private Predicate<String> sampleNameFilter;
private Supplier<Predicate<String>> sampleNameFilterSupplier;
private Authenticator authenticator;

/**
* Port to bind to. Must not be called together with {@link #withInetSocketAddress(InetSocketAddress)}
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand All @@ -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) {
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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<Predicate<String>> sampleNameFilterSupplier) {
private HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon, Supplier<Predicate<String>> 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);
Expand Down
@@ -1,19 +1,25 @@
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;
import java.util.Scanner;
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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
}
}

0 comments on commit c72efdc

Please sign in to comment.