Skip to content

Commit c72efdc

Browse files
committed
Added HTTP authentication to HTTPServer
Signed-off-by: Doug Hoard <[email protected]>
1 parent c3306c4 commit c72efdc

File tree

3 files changed

+122
-7
lines changed

3 files changed

+122
-7
lines changed

simpleclient_httpserver/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,11 @@
5656
<version>2.6.0</version>
5757
<scope>test</scope>
5858
</dependency>
59+
<dependency>
60+
<groupId>javax.xml.bind</groupId>
61+
<artifactId>jaxb-api</artifactId>
62+
<version>2.3.0</version>
63+
<scope>test</scope>
64+
</dependency>
5965
</dependencies>
6066
</project>

simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java

+31-7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.util.concurrent.atomic.AtomicInteger;
2727
import java.util.zip.GZIPOutputStream;
2828

29+
import com.sun.net.httpserver.Authenticator;
30+
import com.sun.net.httpserver.HttpContext;
2931
import com.sun.net.httpserver.HttpExchange;
3032
import com.sun.net.httpserver.HttpHandler;
3133
import com.sun.net.httpserver.HttpServer;
@@ -195,6 +197,7 @@ public static class Builder {
195197
private boolean daemon = false;
196198
private Predicate<String> sampleNameFilter;
197199
private Supplier<Predicate<String>> sampleNameFilterSupplier;
200+
private Authenticator authenticator;
198201

199202
/**
200203
* Port to bind to. Must not be called together with {@link #withInetSocketAddress(InetSocketAddress)}
@@ -286,6 +289,18 @@ public Builder withRegistry(CollectorRegistry registry) {
286289
return this;
287290
}
288291

292+
/**
293+
* Optional: {@link Authenticator} to use to support authentication.
294+
*/
295+
public Builder withAuthenticator(Authenticator authenticator) {
296+
this.authenticator = authenticator;
297+
return this;
298+
}
299+
300+
/**
301+
* Build the HTTPServer
302+
* @throws IOException
303+
*/
289304
public HTTPServer build() throws IOException {
290305
if (sampleNameFilter != null) {
291306
assertNull(sampleNameFilterSupplier, "cannot configure 'sampleNameFilter' and 'sampleNameFilterSupplier' at the same time");
@@ -296,7 +311,7 @@ public HTTPServer build() throws IOException {
296311
assertNull(hostname, "cannot configure 'httpServer' and 'hostname' at the same time");
297312
assertNull(inetAddress, "cannot configure 'httpServer' and 'inetAddress' at the same time");
298313
assertNull(inetSocketAddress, "cannot configure 'httpServer' and 'inetSocketAddress' at the same time");
299-
return new HTTPServer(httpServer, registry, daemon, sampleNameFilterSupplier);
314+
return new HTTPServer(httpServer, registry, daemon, sampleNameFilterSupplier, authenticator);
300315
} else if (inetSocketAddress != null) {
301316
assertZero(port, "cannot configure 'inetSocketAddress' and 'port' at the same time");
302317
assertNull(hostname, "cannot configure 'inetSocketAddress' and 'hostname' at the same time");
@@ -309,7 +324,7 @@ public HTTPServer build() throws IOException {
309324
} else {
310325
inetSocketAddress = new InetSocketAddress(port);
311326
}
312-
return new HTTPServer(HttpServer.create(inetSocketAddress, 3), registry, daemon, sampleNameFilterSupplier);
327+
return new HTTPServer(HttpServer.create(inetSocketAddress, 3), registry, daemon, sampleNameFilterSupplier, authenticator);
313328
}
314329

315330
private void assertNull(Object o, String msg) {
@@ -330,7 +345,7 @@ private void assertZero(int i, String msg) {
330345
* The {@code httpServer} is expected to already be bound to an address
331346
*/
332347
public HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon) throws IOException {
333-
this(httpServer, registry, daemon, null);
348+
this(httpServer, registry, daemon, null, null);
334349
}
335350

336351
/**
@@ -375,15 +390,24 @@ public HTTPServer(String host, int port) throws IOException {
375390
this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, false);
376391
}
377392

378-
private HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon, Supplier<Predicate<String>> sampleNameFilterSupplier) {
393+
private HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon, Supplier<Predicate<String>> sampleNameFilterSupplier, Authenticator authenticator) {
379394
if (httpServer.getAddress() == null)
380395
throw new IllegalArgumentException("HttpServer hasn't been bound to an address");
381396

382397
server = httpServer;
383398
HttpHandler mHandler = new HTTPMetricHandler(registry, sampleNameFilterSupplier);
384-
server.createContext("/", mHandler);
385-
server.createContext("/metrics", mHandler);
386-
server.createContext("/-/healthy", mHandler);
399+
HttpContext mContext = server.createContext("/", mHandler);
400+
if (authenticator != null) {
401+
mContext.setAuthenticator(authenticator);
402+
}
403+
mContext = server.createContext("/metrics", mHandler);
404+
if (authenticator != null) {
405+
mContext.setAuthenticator(authenticator);
406+
}
407+
mContext = server.createContext("/-/healthy", mHandler);
408+
if (authenticator != null) {
409+
mContext.setAuthenticator(authenticator);
410+
}
387411
executorService = Executors.newFixedThreadPool(5, NamedDaemonThreadFactory.defaultThreadFactory(daemon));
388412
server.setExecutor(executorService);
389413
start(daemon);

simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java

+85
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
package io.prometheus.client.exporter;
22

3+
import com.sun.net.httpserver.Authenticator;
4+
import com.sun.net.httpserver.BasicAuthenticator;
35
import com.sun.net.httpserver.HttpServer;
46
import io.prometheus.client.Gauge;
57
import io.prometheus.client.CollectorRegistry;
68
import java.io.IOException;
9+
import java.io.UnsupportedEncodingException;
710
import java.net.InetSocketAddress;
811
import java.net.URL;
912
import java.net.URLConnection;
1013
import java.util.Scanner;
1114
import java.util.zip.GZIPInputStream;
1215

1316
import io.prometheus.client.SampleNameFilter;
17+
import org.junit.Assert;
1418
import org.junit.Before;
1519
import org.junit.Test;
1620

21+
import javax.xml.bind.DatatypeConverter;
22+
1723
import static org.assertj.core.api.Java6Assertions.assertThat;
1824

1925
public class TestHTTPServer {
@@ -67,6 +73,39 @@ String requestWithAccept(HTTPServer s, String accept) throws IOException {
6773
return scanner.hasNext() ? scanner.next() : "";
6874
}
6975

76+
String requestWithCredentials(HTTPServer httpServer, String context, String suffix, String user, String password) throws IOException {
77+
String url = "http://localhost:" + httpServer.server.getAddress().getPort() + context + suffix;
78+
URLConnection connection = new URL(url).openConnection();
79+
connection.setDoOutput(true);
80+
if (user != null && password != null) {
81+
connection.setRequestProperty("Authorization", encodeCredentials(user, password));
82+
}
83+
connection.connect();
84+
Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A");
85+
return s.hasNext() ? s.next() : "";
86+
}
87+
88+
String encodeCredentials(String user, String password) {
89+
// Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8,
90+
try {
91+
byte[] credentialsBytes = (user + ":" + password).getBytes("UTF-8");
92+
String encoded = DatatypeConverter.printBase64Binary(credentialsBytes);
93+
encoded = String.format("Basic %s", encoded);
94+
return encoded;
95+
} catch (UnsupportedEncodingException e) {
96+
throw new IllegalArgumentException(e);
97+
}
98+
}
99+
100+
Authenticator createAuthenticator(String realm, final String validUsername, final String validPassword) {
101+
return new BasicAuthenticator(realm) {
102+
@Override
103+
public boolean checkCredentials(String username, String password) {
104+
return validUsername.equals(username) && validPassword.equals(password);
105+
}
106+
};
107+
}
108+
70109
@Test(expected = IllegalArgumentException.class)
71110
public void testRefuseUsingUnbound() throws IOException {
72111
CollectorRegistry registry = new CollectorRegistry();
@@ -202,4 +241,50 @@ public void testHealthGzipCompression() throws IOException {
202241
s.close();
203242
}
204243
}
244+
245+
@Test
246+
public void testBasicAuthSuccess() throws IOException {
247+
HTTPServer s = new HTTPServer.Builder()
248+
.withRegistry(registry)
249+
.withAuthenticator(createAuthenticator("/", "user", "secret"))
250+
.build();
251+
try {
252+
String response = requestWithCredentials(s, "/metrics","?name[]=a&name[]=b", "user", "secret");
253+
assertThat(response).contains("a 0.0");
254+
} finally {
255+
s.close();
256+
}
257+
}
258+
259+
@Test
260+
public void testBasicAuthCredentialsMissing() throws IOException {
261+
HTTPServer s = new HTTPServer.Builder()
262+
.withRegistry(registry)
263+
.withAuthenticator(createAuthenticator("/", "user", "secret"))
264+
.build();
265+
try {
266+
request(s, "/metrics", "?name[]=a&name[]=b");
267+
Assert.fail("expected IOException with HTTP 401");
268+
} catch (IOException e) {
269+
Assert.assertTrue(e.getMessage().contains("401"));
270+
} finally {
271+
s.close();
272+
}
273+
}
274+
275+
@Test
276+
public void testBasicAuthWrongCredentials() throws IOException {
277+
HTTPServer s = new HTTPServer.Builder()
278+
.withRegistry(registry)
279+
.withAuthenticator(createAuthenticator("/", "user", "wrong"))
280+
.build();
281+
try {
282+
request(s, "/metrics", "?name[]=a&name[]=b");
283+
Assert.fail("expected IOException with HTTP 401");
284+
} catch (IOException e) {
285+
Assert.assertTrue(e.getMessage().contains("401"));
286+
} finally {
287+
s.close();
288+
}
289+
}
205290
}

0 commit comments

Comments
 (0)