Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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;
import com.sun.net.httpserver.HttpsConfigurator;
Expand All @@ -10,14 +11,18 @@
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.security.auth.Subject;

/**
* Expose Prometheus metrics using a plain Java HttpServer.
Expand Down Expand Up @@ -51,16 +56,25 @@ private HTTPServer(
HttpServer httpServer,
PrometheusRegistry registry,
Authenticator authenticator,
String authenticatedSubjectAttributeName,
HttpHandler defaultHandler) {
if (httpServer.getAddress() == null) {
throw new IllegalArgumentException("HttpServer hasn't been bound to an address");
}
this.server = httpServer;
this.executorService = executorService;
registerHandler(
"/", defaultHandler == null ? new DefaultHandler() : defaultHandler, authenticator);
registerHandler("/metrics", new MetricsHandler(config, registry), authenticator);
registerHandler("/-/healthy", new HealthyHandler(), authenticator);
"/",
defaultHandler == null ? new DefaultHandler() : defaultHandler,
authenticator,
authenticatedSubjectAttributeName);
registerHandler(
"/metrics",
new MetricsHandler(config, registry),
authenticator,
authenticatedSubjectAttributeName);
registerHandler(
"/-/healthy", new HealthyHandler(), authenticator, authenticatedSubjectAttributeName);
try {
// HttpServer.start() starts the HttpServer in a new background thread.
// If we call HttpServer.start() from a thread of the executorService,
Expand All @@ -74,13 +88,54 @@ private HTTPServer(
}
}

private void registerHandler(String path, HttpHandler handler, Authenticator authenticator) {
HttpContext context = server.createContext(path, handler);
private void registerHandler(
String path, HttpHandler handler, Authenticator authenticator, String subjectAttributeName) {
HttpContext context = server.createContext(path, wrapWithDoAs(handler, subjectAttributeName));
if (authenticator != null) {
context.setAuthenticator(authenticator);
}
}

private HttpHandler wrapWithDoAs(HttpHandler handler, String subjectAttributeName) {
if (subjectAttributeName == null) {
return handler;
}

// invoke handler using the subject.doAs from the named attribute
return new HttpHandler() {
@Override
public void handle(HttpExchange exchange) throws IOException {
Object authSubject = exchange.getAttribute(subjectAttributeName);
if (authSubject instanceof Subject) {
try {
Subject.doAs(
(Subject) authSubject,
(PrivilegedExceptionAction<IOException>)
() -> {
handler.handle(exchange);
return null;
});
} catch (PrivilegedActionException e) {
if (e.getException() != null) {
throw new IOException(e.getException());
} else throw new IOException(e);
}
} else {
drainInputAndClose(exchange);
exchange.sendResponseHeaders(403, -1);
}
}
};
}

private void drainInputAndClose(HttpExchange httpExchange) throws IOException {
InputStream inputStream = httpExchange.getRequestBody();
byte[] b = new byte[4096];
while (inputStream.read(b) != -1)
;
inputStream.close();
}

/** Stop the HTTP server. Same as {@link #close()}. */
public void stop() {
close();
Expand Down Expand Up @@ -120,6 +175,7 @@ public static class Builder {
private Authenticator authenticator = null;
private HttpsConfigurator httpsConfigurator = null;
private HttpHandler defaultHandler = null;
private String authenticatedSubjectAttributeName = null;

private Builder(PrometheusProperties config) {
this.config = config;
Expand Down Expand Up @@ -171,6 +227,12 @@ public Builder authenticator(Authenticator authenticator) {
return this;
}

/** Optional: the attribute name of a Subject from a custom authenticator. */
public Builder authenticatedSubjectAttributeName(String authenticatedSubjectAttributeName) {
this.authenticatedSubjectAttributeName = authenticatedSubjectAttributeName;
return this;
}

/** Optional: {@link HttpsConfigurator} for TLS/SSL */
public Builder httpsConfigurator(HttpsConfigurator configurator) {
this.httpsConfigurator = configurator;
Expand Down Expand Up @@ -201,7 +263,13 @@ public HTTPServer buildAndStart() throws IOException {
ExecutorService executorService = makeExecutorService();
httpServer.setExecutor(executorService);
return new HTTPServer(
config, executorService, httpServer, registry, authenticator, defaultHandler);
config,
executorService,
httpServer,
registry,
authenticator,
authenticatedSubjectAttributeName,
defaultHandler);
}

private InetSocketAddress makeInetSocketAddress() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.prometheus.metrics.exporter.httpserver;

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

import com.sun.net.httpserver.Authenticator;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpPrincipal;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.Principal;
import javax.security.auth.Subject;
import org.junit.jupiter.api.Test;

public class HTTPServerTest {

@Test
public void testSubjectDoAs() throws Exception {

final String user = "joe";
final Subject subject = new Subject();
subject.getPrincipals().add(() -> (user));

Authenticator authenticator =
new Authenticator() {
@Override
public Result authenticate(HttpExchange exchange) {
exchange.setAttribute("aa", subject);
return new Success(new HttpPrincipal(user, "/"));
}
};

HttpHandler handler =
exchange -> {
boolean found = false;
Subject current = Subject.getSubject(AccessController.getContext());
for (Principal p : current.getPrincipals()) {
if (user.equals(p.getName())) {
found = true;
break;
}
}
if (!found) {
throw new IOException("Expected validated user joe!");
}
exchange.sendResponseHeaders(204, -1);
};
HTTPServer server =
HTTPServer.builder()
.port(0)
.authenticator(authenticator)
.defaultHandler(handler)
.authenticatedSubjectAttributeName("aa")
.buildAndStart();

Socket socket = new Socket();
try {
socket.connect(new InetSocketAddress("localhost", server.getPort()));

socket.getOutputStream().write("GET / HTTP/1.1 \r\n".getBytes(StandardCharsets.UTF_8));
socket.getOutputStream().write("HOST: localhost \r\n\r\n".getBytes(StandardCharsets.UTF_8));
socket.getOutputStream().flush();

String actualResponse = "";
byte[] resp = new byte[500];
int read = socket.getInputStream().read(resp, 0, resp.length);
if (read > 0) {
actualResponse = new String(resp, 0, read);
}
assertThat(actualResponse).contains("204");

} finally {
socket.close();
}
}
}