Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added HTTP authentication to HTTPServer #682

Merged
merged 1 commit into from
Aug 29, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions simpleclient_httpserver/pom.xml
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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<String> sampleNameFilter;
private Supplier<Predicate<String>> 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<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);
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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();
}
}
}