Skip to content

Commit 336e770

Browse files
committed
# This is a combination of 3 commits.
# This is the 1st commit message: Added HTTP authentication to HTTPServer Signed-off-by: Doug Hoard <doug.hoard@gmail.com> # This is the commit message #2: Added BasicAuthenticator for HTTPServer Signed-off-by: Doug Hoard <doug.hoard@gmail.com> # This is the commit message #3: Simplified code
1 parent c3306c4 commit 336e770

File tree

5 files changed

+226
-7
lines changed

5 files changed

+226
-7
lines changed

simpleclient_httpserver/pom.xml

Lines changed: 6 additions & 0 deletions
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

Lines changed: 31 additions & 7 deletions
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);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.prometheus.client.exporter;
2+
3+
/**
4+
* Basic HTTP authenticator
5+
*/
6+
public class HTTPServerBasicAuthenticator extends com.sun.net.httpserver.BasicAuthenticator {
7+
8+
private static final String REALM = "/";
9+
10+
private String username;
11+
private String password;
12+
13+
public HTTPServerBasicAuthenticator(String username, String password) {
14+
super(REALM);
15+
16+
if ((username == null) || (username.trim().length() == 0)) {
17+
throw new IllegalArgumentException("username is null or empty");
18+
}
19+
20+
if ((password == null) || (password.trim().length() == 0)) {
21+
throw new IllegalArgumentException("password is null or empty");
22+
}
23+
24+
this.username = username;
25+
this.password = password;
26+
}
27+
28+
/**
29+
* Check username and password
30+
* @param username
31+
* @param password
32+
* @return
33+
*/
34+
@Override
35+
public boolean checkCredentials(String username, String password) {
36+
return (this.username.equals(username) && this.password.equals(password));
37+
}
38+
}

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44
import io.prometheus.client.Gauge;
55
import io.prometheus.client.CollectorRegistry;
66
import java.io.IOException;
7+
import java.io.UnsupportedEncodingException;
78
import java.net.InetSocketAddress;
89
import java.net.URL;
910
import java.net.URLConnection;
1011
import java.util.Scanner;
1112
import java.util.zip.GZIPInputStream;
1213

1314
import io.prometheus.client.SampleNameFilter;
15+
import org.junit.Assert;
1416
import org.junit.Before;
1517
import org.junit.Test;
1618

19+
import javax.xml.bind.DatatypeConverter;
20+
1721
import static org.assertj.core.api.Java6Assertions.assertThat;
1822

1923
public class TestHTTPServer {
@@ -67,6 +71,30 @@ String requestWithAccept(HTTPServer s, String accept) throws IOException {
6771
return scanner.hasNext() ? scanner.next() : "";
6872
}
6973

74+
String requestWithCredentials(HTTPServer httpServer, String context, String suffix, String username, String password) throws IOException {
75+
String url = "http://localhost:" + httpServer.server.getAddress().getPort() + context + suffix;
76+
URLConnection connection = new URL(url).openConnection();
77+
connection.setDoOutput(true);
78+
if (username != null && password != null) {
79+
connection.setRequestProperty("Authorization", encodeCredentials(username, password));
80+
}
81+
connection.connect();
82+
Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A");
83+
return s.hasNext() ? s.next() : "";
84+
}
85+
86+
String encodeCredentials(String user, String password) {
87+
// Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8,
88+
try {
89+
byte[] credentialsBytes = (user + ":" + password).getBytes("UTF-8");
90+
String encoded = DatatypeConverter.printBase64Binary(credentialsBytes);
91+
encoded = String.format("Basic %s", encoded);
92+
return encoded;
93+
} catch (UnsupportedEncodingException e) {
94+
throw new IllegalArgumentException(e);
95+
}
96+
}
97+
7098
@Test(expected = IllegalArgumentException.class)
7199
public void testRefuseUsingUnbound() throws IOException {
72100
CollectorRegistry registry = new CollectorRegistry();
@@ -202,4 +230,59 @@ public void testHealthGzipCompression() throws IOException {
202230
s.close();
203231
}
204232
}
233+
234+
@Test
235+
public void testBasicAuthSuccess() throws IOException {
236+
String username = "user";
237+
String password = "secret";
238+
HTTPServerBasicAuthenticator authenticator = new HTTPServerBasicAuthenticator(username, password);
239+
HTTPServer s = new HTTPServer.Builder()
240+
.withRegistry(registry)
241+
.withAuthenticator(authenticator)
242+
.build();
243+
try {
244+
String response = requestWithCredentials(s, "/metrics","?name[]=a&name[]=b", "user", "secret");
245+
assertThat(response).contains("a 0.0");
246+
} finally {
247+
s.close();
248+
}
249+
}
250+
251+
@Test
252+
public void testBasicAuthCredentialsMissing() throws IOException {
253+
String username = "user";
254+
String password = "secret";
255+
HTTPServerBasicAuthenticator authenticator = new HTTPServerBasicAuthenticator(username, password);
256+
HTTPServer s = new HTTPServer.Builder()
257+
.withRegistry(registry)
258+
.withAuthenticator(authenticator)
259+
.build();
260+
try {
261+
request(s, "/metrics", "?name[]=a&name[]=b");
262+
Assert.fail("expected IOException with HTTP 401");
263+
} catch (IOException e) {
264+
Assert.assertTrue(e.getMessage().contains("401"));
265+
} finally {
266+
s.close();
267+
}
268+
}
269+
270+
@Test
271+
public void testBasicAuthWrongCredentials() throws IOException {
272+
String username = "user";
273+
String password = "secret";
274+
HTTPServerBasicAuthenticator authenticator = new HTTPServerBasicAuthenticator(username, password);
275+
HTTPServer s = new HTTPServer.Builder()
276+
.withRegistry(registry)
277+
.withAuthenticator(authenticator)
278+
.build();
279+
try {
280+
requestWithCredentials(s, "/metrics", "?name[]=a&name[]=b", username, "wrong");
281+
Assert.fail("expected IOException with HTTP 401");
282+
} catch (IOException e) {
283+
Assert.assertTrue(e.getMessage().contains("401"));
284+
} finally {
285+
s.close();
286+
}
287+
}
205288
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.prometheus.client.exporter;
2+
3+
import org.junit.Assert;
4+
import org.junit.Test;
5+
6+
import static org.assertj.core.api.Java6Assertions.assertThat;
7+
8+
public class TestHTTPServerBasicAuthenticator {
9+
10+
@Test
11+
public void testUsernameAndPassword() {
12+
HTTPServerBasicAuthenticator authenticator = new HTTPServerBasicAuthenticator("test", "secret");
13+
Assert.assertTrue(authenticator.checkCredentials("test", "secret"));
14+
}
15+
16+
@Test
17+
public void testWrongUsername() {
18+
HTTPServerBasicAuthenticator authenticator = new HTTPServerBasicAuthenticator("test", "secret");
19+
20+
Assert.assertFalse(authenticator.checkCredentials("wrong", "secret"));
21+
}
22+
23+
@Test
24+
public void testWrongPassword() {
25+
HTTPServerBasicAuthenticator authenticator = new HTTPServerBasicAuthenticator("test", "secret");
26+
Assert.assertFalse(authenticator.checkCredentials("test", "wrong"));
27+
}
28+
29+
@Test
30+
public void testNullUsername() {
31+
try {
32+
new HTTPServerBasicAuthenticator(null, "secret");
33+
Assert.fail("Expected IllegalArgumentException");
34+
} catch (IllegalArgumentException e) {
35+
assertThat(e.getMessage().contains("username is null or empty"));
36+
}
37+
}
38+
39+
@Test
40+
public void testEmptyUsername() {
41+
try {
42+
new HTTPServerBasicAuthenticator("", "secret");
43+
Assert.fail("Expected IllegalArgumentException");
44+
} catch (IllegalArgumentException e) {
45+
assertThat(e.getMessage().contains("username is null or empty"));
46+
}
47+
}
48+
49+
@Test
50+
public void testNullPassword() {
51+
try {
52+
new HTTPServerBasicAuthenticator("test", null);
53+
Assert.fail("Expected IllegalArgumentException");
54+
} catch (IllegalArgumentException e) {
55+
assertThat(e.getMessage().contains("password is null or empty"));
56+
}
57+
}
58+
59+
@Test
60+
public void testEmptyPassword() {
61+
try {
62+
new HTTPServerBasicAuthenticator("test", "");
63+
Assert.fail("Expected IllegalArgumentException");
64+
} catch (IllegalArgumentException e) {
65+
assertThat(e.getMessage().contains("password is null or empty"));
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)