Skip to content

Commit 94cd5f3

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

File tree

4 files changed

+256
-13
lines changed

4 files changed

+256
-13
lines changed

simpleclient/src/test/java/io/prometheus/client/HistogramTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ private void assertExemplar(Histogram histogram, double value, String... labels)
299299
}
300300
if (lowerBound < value && value <= upperBound) {
301301
Assert.assertNotNull("No exemplar found in bucket [" + lowerBound + ", " + upperBound + "]", bucket.exemplar);
302-
Assert.assertEquals(value, bucket.exemplar.getValue(), 0.001);
302+
Assert.assertEquals(value, bucket.exemplar.getValue(), 0.006);
303303
Assert.assertEquals(labels.length/2, bucket.exemplar.getNumberOfLabels());
304304
for (int i=0; i<labels.length; i+=2) {
305305
Assert.assertEquals(labels[i], bucket.exemplar.getLabelName(i/2));

simpleclient_httpserver/pom.xml

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

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

+42-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.prometheus.client.exporter;
22

3+
import com.sun.net.httpserver.*;
34
import io.prometheus.client.CollectorRegistry;
45
import io.prometheus.client.exporter.common.TextFormat;
56

@@ -21,10 +22,6 @@
2122
import java.util.concurrent.atomic.AtomicInteger;
2223
import java.util.zip.GZIPOutputStream;
2324

24-
import com.sun.net.httpserver.HttpExchange;
25-
import com.sun.net.httpserver.HttpHandler;
26-
import com.sun.net.httpserver.HttpServer;
27-
2825
/**
2926
* Expose Prometheus metrics using a plain Java HttpServer.
3027
* <p>
@@ -166,42 +163,75 @@ static ThreadFactory defaultThreadFactory(boolean daemon) {
166163
protected final ExecutorService executorService;
167164

168165
/**
169-
* Start a HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}.
166+
* Start a HTTP server serving Prometheus metrics from the given registry using the given {@link Authenticator}
167+
* and {@link HttpServer}.
170168
* The {@code httpServer} is expected to already be bound to an address
171169
*/
172-
public HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon) throws IOException {
170+
public HTTPServer(HttpServer httpServer, Authenticator authenticator, CollectorRegistry registry, boolean daemon) throws IOException {
173171
if (httpServer.getAddress() == null)
174172
throw new IllegalArgumentException("HttpServer hasn't been bound to an address");
175173

176174
server = httpServer;
177175
HttpHandler mHandler = new HTTPMetricHandler(registry);
178-
server.createContext("/", mHandler);
179-
server.createContext("/metrics", mHandler);
180-
server.createContext("/-/healthy", mHandler);
176+
HttpContext httpContext = server.createContext("/", mHandler);
177+
if (authenticator != null) {
178+
httpContext.setAuthenticator(authenticator);
179+
}
180+
httpContext = server.createContext("/metrics", mHandler);
181+
if (authenticator != null) {
182+
httpContext.setAuthenticator(authenticator);
183+
}
184+
httpContext = server.createContext("/-/healthy", mHandler);
185+
if (authenticator != null) {
186+
httpContext.setAuthenticator(authenticator);
187+
}
181188
executorService = Executors.newFixedThreadPool(5, NamedDaemonThreadFactory.defaultThreadFactory(daemon));
182189
server.setExecutor(executorService);
183190
start(daemon);
184191
}
185192

193+
/**
194+
* Start a HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}.
195+
* The {@code httpServer} is expected to already be bound to an address
196+
*/
197+
public HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon) throws IOException {
198+
this(httpServer, null, registry, daemon);
199+
}
200+
201+
/**
202+
* Start a HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}.
203+
* The {@code httpServer} is expected to already be bound to an address
204+
*/
205+
public HTTPServer(HttpServer httpServer, CollectorRegistry registry) throws IOException {
206+
this(httpServer, null, registry, false);
207+
}
208+
209+
/**
210+
* Start a HTTP server serving Prometheus metrics from the given registry.
211+
*/
212+
public HTTPServer(InetSocketAddress addr, Authenticator authenticator, CollectorRegistry registry, boolean daemon) throws IOException {
213+
this(HttpServer.create(addr, 3), authenticator, registry, daemon);
214+
}
215+
186216
/**
187217
* Start a HTTP server serving Prometheus metrics from the given registry.
188218
*/
189219
public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException {
190-
this(HttpServer.create(addr, 3), registry, daemon);
220+
this(HttpServer.create(addr, 3), null, registry, daemon);
191221
}
192222

193223
/**
194224
* Start a HTTP server serving Prometheus metrics from the given registry using non-daemon threads.
195225
*/
196226
public HTTPServer(InetSocketAddress addr, CollectorRegistry registry) throws IOException {
197-
this(addr, registry, false);
227+
this(addr, null, registry, false);
198228
}
199229

200230
/**
201231
* Start a HTTP server serving the default Prometheus registry.
202232
*/
203233
public HTTPServer(int port, boolean daemon) throws IOException {
204-
this(new InetSocketAddress(port), CollectorRegistry.defaultRegistry, daemon);
234+
this(new InetSocketAddress(port), null, CollectorRegistry.defaultRegistry, daemon);
205235
}
206236

207237
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package io.prometheus.client.exporter;
2+
3+
import com.sun.net.httpserver.Authenticator;
4+
import com.sun.net.httpserver.BasicAuthenticator;
5+
import com.sun.net.httpserver.HttpServer;
6+
import io.prometheus.client.CollectorRegistry;
7+
import io.prometheus.client.Gauge;
8+
import org.junit.After;
9+
import org.junit.Before;
10+
import org.junit.Test;
11+
12+
import javax.net.ssl.SSLContext;
13+
import javax.xml.bind.DatatypeConverter;
14+
import java.io.IOException;
15+
import java.io.UnsupportedEncodingException;
16+
import java.net.InetSocketAddress;
17+
import java.net.URL;
18+
import java.net.URLConnection;
19+
import java.util.Scanner;
20+
import java.util.zip.GZIPInputStream;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.assertj.core.api.Assertions.fail;
24+
25+
public class TestHTTPServerBasicAuthentication {
26+
27+
private static final String HTTP_USER = "prometheus";
28+
private static final String HTTP_PASSWORD = "some_password";
29+
30+
HTTPServer s;
31+
32+
@Before
33+
public void init() throws IOException {
34+
CollectorRegistry registry = new CollectorRegistry();
35+
Gauge.build("a", "a help").register(registry);
36+
Gauge.build("b", "a help").register(registry);
37+
Gauge.build("c", "a help").register(registry);
38+
39+
Authenticator authenticator = new BasicAuthenticator("/") {
40+
@Override
41+
public boolean checkCredentials(String user, String password) {
42+
return HTTP_USER.equals(user) && HTTP_PASSWORD.equals(password);
43+
}
44+
};
45+
46+
SSLContext sslContext = null;
47+
48+
HttpServer httpServer = HttpServer.create(new InetSocketAddress(0), 3);
49+
s = new HTTPServer(httpServer, authenticator, registry, false);
50+
}
51+
52+
@After
53+
public void cleanup() {
54+
s.stop();
55+
}
56+
57+
String request(String context, String suffix) throws IOException {
58+
return request(context, suffix, HTTP_USER, HTTP_PASSWORD);
59+
}
60+
61+
String request(String context, String suffix, String user, String password) throws IOException {
62+
String url = "http://localhost:" + s.server.getAddress().getPort() + context + suffix;
63+
URLConnection connection = new URL(url).openConnection();
64+
connection.setDoOutput(true);
65+
if (user != null && password != null) {
66+
connection.setRequestProperty("Authorization", encodeCredentials(user, password));
67+
}
68+
connection.connect();
69+
Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A");
70+
return s.hasNext() ? s.next() : "";
71+
}
72+
73+
String request(String suffix) throws IOException {
74+
return request("/metrics", suffix);
75+
}
76+
77+
String requestWithCompression(String suffix) throws IOException {
78+
return requestWithCompression("/metrics", suffix);
79+
}
80+
81+
String requestWithCompression(String context, String suffix) throws IOException {
82+
return requestWithCompression(context, suffix, HTTP_USER, HTTP_PASSWORD);
83+
}
84+
85+
String requestWithCompression(String context, String suffix, String user, String password) throws IOException {
86+
String url = "http://localhost:" + s.server.getAddress().getPort() + context + suffix;
87+
URLConnection connection = new URL(url).openConnection();
88+
connection.setDoOutput(true);
89+
connection.setDoInput(true);
90+
if (user != null && password != null) {
91+
connection.setRequestProperty("Authorization", encodeCredentials(user, password));
92+
}
93+
connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
94+
connection.connect();
95+
GZIPInputStream gzs = new GZIPInputStream(connection.getInputStream());
96+
Scanner s = new Scanner(gzs).useDelimiter("\\A");
97+
return s.hasNext() ? s.next() : "";
98+
}
99+
100+
String requestWithAccept(String accept) throws IOException {
101+
return requestWithAccept(accept, HTTP_USER, HTTP_PASSWORD);
102+
}
103+
104+
String requestWithAccept(String accept, String user, String password) throws IOException {
105+
String url = "http://localhost:" + s.server.getAddress().getPort();
106+
URLConnection connection = new URL(url).openConnection();
107+
connection.setDoOutput(true);
108+
connection.setDoInput(true);
109+
if (user != null && password != null) {
110+
connection.setRequestProperty("Authorization", encodeCredentials(user, password));
111+
}
112+
connection.setRequestProperty("Accept", accept);
113+
Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A");
114+
return s.hasNext() ? s.next() : "";
115+
}
116+
117+
private static String encodeCredentials(String user, String password) {
118+
// Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8,
119+
try {
120+
byte[] credentialsBytes = (user + ":" + password).getBytes("UTF-8");
121+
String encoded = DatatypeConverter.printBase64Binary(credentialsBytes);
122+
encoded = String.format("Basic %s", encoded);
123+
return encoded;
124+
} catch (UnsupportedEncodingException e) {
125+
throw new IllegalArgumentException(e);
126+
}
127+
}
128+
129+
@Test(expected = IllegalArgumentException.class)
130+
public void testRefuseUsingUnbound() throws IOException {
131+
CollectorRegistry registry = new CollectorRegistry();
132+
HTTPServer s = new HTTPServer(HttpServer.create(), registry, true);
133+
s.stop();
134+
}
135+
136+
@Test
137+
public void testSimpleRequest() throws IOException {
138+
String response = request("");
139+
assertThat(response).contains("a 0.0");
140+
assertThat(response).contains("b 0.0");
141+
assertThat(response).contains("c 0.0");
142+
}
143+
144+
@Test
145+
public void testBadParams() throws IOException {
146+
String response = request("?x");
147+
assertThat(response).contains("a 0.0");
148+
assertThat(response).contains("b 0.0");
149+
assertThat(response).contains("c 0.0");
150+
}
151+
152+
@Test
153+
public void testSingleName() throws IOException {
154+
String response = request("?name[]=a");
155+
assertThat(response).contains("a 0.0");
156+
assertThat(response).doesNotContain("b 0.0");
157+
assertThat(response).doesNotContain("c 0.0");
158+
}
159+
160+
@Test
161+
public void testMultiName() throws IOException {
162+
String response = request("?name[]=a&name[]=b");
163+
assertThat(response).contains("a 0.0");
164+
assertThat(response).contains("b 0.0");
165+
assertThat(response).doesNotContain("c 0.0");
166+
}
167+
168+
@Test
169+
public void testDecoding() throws IOException {
170+
String response = request("?n%61me[]=%61");
171+
assertThat(response).contains("a 0.0");
172+
assertThat(response).doesNotContain("b 0.0");
173+
assertThat(response).doesNotContain("c 0.0");
174+
}
175+
176+
@Test
177+
public void testGzipCompression() throws IOException {
178+
String response = requestWithCompression("");
179+
assertThat(response).contains("a 0.0");
180+
assertThat(response).contains("b 0.0");
181+
assertThat(response).contains("c 0.0");
182+
}
183+
184+
@Test
185+
public void testOpenMetrics() throws IOException {
186+
String response = requestWithAccept("application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1");
187+
assertThat(response).contains("# EOF");
188+
}
189+
190+
@Test
191+
public void testHealth() throws IOException {
192+
String response = request("/-/healthy", "");
193+
assertThat(response).contains("Exporter is Healthy");
194+
}
195+
196+
@Test
197+
public void testHealthGzipCompression() throws IOException {
198+
String response = requestWithCompression("/-/healthy", "");
199+
assertThat(response).contains("Exporter is Healthy");
200+
}
201+
202+
@Test(expected = IOException.class)
203+
public void testHealthyBasicAuthenticationFailure() throws IOException {
204+
String response = request("/-/healthy", "", null, null);
205+
fail("Expected IOException");
206+
}
207+
}

0 commit comments

Comments
 (0)