Skip to content
Open
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.prometheus.metrics.exporter.common;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

class InvalidQueryParameterException extends RuntimeException {

InvalidQueryParameterException(String message) {
super(message);
}

// decode with Charset is only available in Java 10+, but we want to support Java 8
@SuppressWarnings("JdkObsolete")
static String urlDecode(String value) throws UnsupportedEncodingException {
try {
return URLDecoder.decode(value, "UTF-8");
} catch (IllegalArgumentException e) {
throw new InvalidQueryParameterException("Invalid percent-encoding in query string");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import io.prometheus.metrics.annotations.StableApi;
import io.prometheus.metrics.model.registry.PrometheusScrapeRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -49,15 +48,43 @@ default String getParameter(String name) {
@SuppressWarnings("JdkObsolete")
default String[] getParameterValues(String name) {
try {
int maxQueryStringLength = 64 * 1024;
int maxQueryParameterCount = 1024;
ArrayList<String> result = new ArrayList<>();
String queryString = getQueryString();
if (queryString != null) {
String[] pairs = queryString.split("&");
for (String pair : pairs) {
if (queryString.length() > maxQueryStringLength) {
throw new InvalidQueryParameterException(
"Query string too long: "
+ queryString.length()
+ " characters (max "
+ maxQueryStringLength
+ ")");
}
int start = 0;
int parameterCount = 0;
for (int end = queryString.indexOf('&'); start <= queryString.length(); ) {
parameterCount++;
if (parameterCount > maxQueryParameterCount) {
throw new InvalidQueryParameterException(
"Too many query parameters: "
+ parameterCount
+ " (max "
+ maxQueryParameterCount
+ ")");
}
String pair =
end == -1 ? queryString.substring(start) : queryString.substring(start, end);
int idx = pair.indexOf("=");
if (idx != -1 && URLDecoder.decode(pair.substring(0, idx), "UTF-8").equals(name)) {
result.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
if (idx != -1
&& InvalidQueryParameterException.urlDecode(pair.substring(0, idx)).equals(name)) {
result.add(InvalidQueryParameterException.urlDecode(pair.substring(idx + 1)));
}
if (end == -1) {
break;
}
start = end + 1;
end = queryString.indexOf('&', start);
}
}
if (result.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,19 @@ public PrometheusScrapeHandler(PrometheusProperties config, PrometheusRegistry r
public void handleRequest(PrometheusHttpExchange exchange) throws IOException {
try {
PrometheusHttpRequest request = exchange.getRequest();
MetricSnapshots snapshots = scrape(request);
String[] includedNames = null;
String debugParam = null;
try {
includedNames = request.getParameterValues("name[]");
debugParam = request.getParameter("debug");
} catch (InvalidQueryParameterException e) {
writeInvalidQueryParametersResponse(exchange);
return;
}
MetricSnapshots snapshots = scrape(request, includedNames);
String acceptHeader = request.getHeader("Accept");
EscapingScheme escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeader);
if (writeDebugResponse(snapshots, escapingScheme, exchange)) {
if (writeDebugResponse(snapshots, escapingScheme, debugParam, exchange)) {
return;
}
ExpositionFormatWriter writer = expositionFormats.findWriter(acceptHeader);
Expand Down Expand Up @@ -136,9 +145,9 @@ private Predicate<String> makeNameFilter(@Nullable String[] includedNames) {
return result;
}

private MetricSnapshots scrape(PrometheusHttpRequest request) {
private MetricSnapshots scrape(PrometheusHttpRequest request, @Nullable String[] includedNames) {

Predicate<String> filter = makeNameFilter(request.getParameterValues("name[]"));
Predicate<String> filter = makeNameFilter(includedNames);
if (filter != null) {
return registry.scrape(filter, request);
} else {
Expand All @@ -147,9 +156,11 @@ private MetricSnapshots scrape(PrometheusHttpRequest request) {
}

private boolean writeDebugResponse(
MetricSnapshots snapshots, EscapingScheme escapingScheme, PrometheusHttpExchange exchange)
MetricSnapshots snapshots,
EscapingScheme escapingScheme,
@Nullable String debugParam,
PrometheusHttpExchange exchange)
throws IOException {
String debugParam = exchange.getRequest().getParameter("debug");
PrometheusHttpResponse response = exchange.getResponse();
if (debugParam == null) {
return false;
Expand Down Expand Up @@ -184,6 +195,16 @@ private boolean writeDebugResponse(
}
}

private void writeInvalidQueryParametersResponse(PrometheusHttpExchange exchange)
throws IOException {
PrometheusHttpResponse response = exchange.getResponse();
response.setHeader("Content-Type", "text/plain; charset=utf-8");
byte[] message = "Invalid query parameters".getBytes(StandardCharsets.UTF_8);
try (OutputStream outputStream = response.sendHeadersAndGetBody(400, message.length)) {
outputStream.write(message);
}
}

private boolean shouldUseCompression(PrometheusHttpRequest request) {
if (preferUncompressedResponse) {
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.prometheus.metrics.exporter.common;

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

import java.util.Collections;
import java.util.Enumeration;
Expand Down Expand Up @@ -80,6 +81,22 @@ void testGetParameterValuesIgnoresParametersWithoutEquals() {
assertThat(values).containsExactly("value1", "value2");
}

@Test
void testGetParameterValuesRejectsMalformedEncodedName() {
PrometheusHttpRequest request = new TestPrometheusHttpRequest("name%ZZ=value");

assertThatExceptionOfType(InvalidQueryParameterException.class)
.isThrownBy(() -> request.getParameterValues("name"));
}

@Test
void testGetParameterValuesRejectsMalformedEncodedValue() {
PrometheusHttpRequest request = new TestPrometheusHttpRequest("name=value%ZZ");

assertThatExceptionOfType(InvalidQueryParameterException.class)
.isThrownBy(() -> request.getParameterValues("name"));
}

/** Test implementation of PrometheusHttpRequest for testing default methods. */
private static class TestPrometheusHttpRequest implements PrometheusHttpRequest {
private final String queryString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,36 @@ void testMultipleMetricNameFilters() throws IOException {
assertThat(body).doesNotContain("metric_three");
}

@Test
void testRejectsTooManyQueryParameters() throws IOException {
StringBuilder queryString = new StringBuilder("name[]=test_counter");
for (int i = 0; i < 1024; i++) {
queryString.append("&name[]=metric_").append(i);
}

TestHttpExchange exchange = new TestHttpExchange("GET", queryString.toString());
handler.handleRequest(exchange);

assertThat(exchange.getResponseCode()).isEqualTo(400);
assertThat(exchange.getResponseHeaders().get("Content-Type"))
.isEqualTo("text/plain; charset=utf-8");
assertThat(exchange.getResponseBody()).isEqualTo("Invalid query parameters");
}

@Test
void testRejectsTooLongQueryString() throws IOException {
StringBuilder queryString = new StringBuilder("name[]=");
for (int i = 0; i < 64 * 1024; i++) {
queryString.append("a");
}

TestHttpExchange exchange = new TestHttpExchange("GET", queryString.toString());
handler.handleRequest(exchange);

assertThat(exchange.getResponseCode()).isEqualTo(400);
assertThat(exchange.getResponseBody()).isEqualTo("Invalid query parameters");
}

/** Test implementation of PrometheusHttpExchange for testing. */
private static class TestHttpExchange implements PrometheusHttpExchange {
private final TestHttpRequest request;
Expand Down Expand Up @@ -216,7 +246,7 @@ public Map<String, String> getResponseHeaders() {
}

public String getResponseBody() {
return rawResponseBody.toString(StandardCharsets.UTF_8);
return new String(rawResponseBody.toByteArray(), StandardCharsets.UTF_8);
}

public boolean isGzipCompressed() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.function.Predicate;
import javax.annotation.Nullable;

Expand All @@ -26,10 +27,10 @@ private MetricNameFilter(
Collection<String> nameIsNotEqualTo,
Collection<String> nameStartsWith,
Collection<String> nameDoesNotStartWith) {
this.nameIsEqualTo = unmodifiableCollection(new ArrayList<>(nameIsEqualTo));
this.nameIsNotEqualTo = unmodifiableCollection(new ArrayList<>(nameIsNotEqualTo));
this.nameStartsWith = unmodifiableCollection(new ArrayList<>(nameStartsWith));
this.nameDoesNotStartWith = unmodifiableCollection(new ArrayList<>(nameDoesNotStartWith));
this.nameIsEqualTo = unmodifiableCollection(new LinkedHashSet<>(nameIsEqualTo));
this.nameIsNotEqualTo = unmodifiableCollection(new LinkedHashSet<>(nameIsNotEqualTo));
this.nameStartsWith = unmodifiableCollection(new LinkedHashSet<>(nameStartsWith));
this.nameDoesNotStartWith = unmodifiableCollection(new LinkedHashSet<>(nameDoesNotStartWith));
}

@Override
Expand All @@ -44,6 +45,9 @@ private boolean matchesNameEqualTo(String metricName) {
if (nameIsEqualTo.isEmpty()) {
return true;
}
if (nameIsEqualTo.contains(metricName)) {
return true;
}
for (String name : nameIsEqualTo) {
// The following ignores suffixes like _total.
// "request_count" and "request_count_total" both match a metric named "request_count".
Expand All @@ -58,6 +62,9 @@ private boolean matchesNameNotEqualTo(String metricName) {
if (nameIsNotEqualTo.isEmpty()) {
return false;
}
if (nameIsNotEqualTo.contains(metricName)) {
return true;
}
for (String name : nameIsNotEqualTo) {
// The following ignores suffixes like _total.
// "request_count" and "request_count_total" both match a metric named "request_count".
Expand Down