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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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();
}
}
}