Skip to content

Commit f0e0d6f

Browse files
committed
Rewrite endpoints urls to https when management-url uses https
Recently a lot of people had issues when using https but the actuator- index falsely reported http addresses. This commits will align them to also use https.
1 parent 7a9cec0 commit f0e0d6f

File tree

4 files changed

+144
-32
lines changed

4 files changed

+144
-32
lines changed

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/domain/values/Endpoints.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2018 the original author or authors.
2+
* Copyright 2014-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Optional;
2626
import java.util.function.Function;
27+
import java.util.stream.Stream;
2728
import javax.annotation.Nullable;
2829

2930
import static java.util.stream.Collectors.toMap;
@@ -43,16 +44,16 @@ private Endpoints(Collection<Endpoint> endpoints) {
4344
}
4445

4546
public Optional<Endpoint> get(String id) {
46-
return Optional.ofNullable(endpoints.get(id));
47+
return Optional.ofNullable(this.endpoints.get(id));
4748
}
4849

4950
public boolean isPresent(String id) {
50-
return endpoints.containsKey(id);
51+
return this.endpoints.containsKey(id);
5152
}
5253

5354
@Override
5455
public Iterator<Endpoint> iterator() {
55-
return new UnmodifiableIterator<>(endpoints.values().iterator());
56+
return new UnmodifiableIterator<>(this.endpoints.values().iterator());
5657
}
5758

5859
public static Endpoints empty() {
@@ -77,6 +78,10 @@ public Endpoints withEndpoint(String id, String url) {
7778
return new Endpoints(newEndpoints.values());
7879
}
7980

81+
public Stream<Endpoint> stream() {
82+
return this.endpoints.values().stream();
83+
}
84+
8085
private static class UnmodifiableIterator<T> implements Iterator<T> {
8186
private final Iterator<T> delegate;
8287

@@ -86,12 +91,12 @@ private UnmodifiableIterator(Iterator<T> delegate) {
8691

8792
@Override
8893
public boolean hasNext() {
89-
return delegate.hasNext();
94+
return this.delegate.hasNext();
9095
}
9196

9297
@Override
9398
public T next() {
94-
return delegate.next();
99+
return this.delegate.next();
95100
}
96101
}
97102
}

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/endpoints/ProbeEndpointsStrategy.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public Mono<Endpoints> detectEndpoints(Instance instance) {
6565
.flatMap(this::convert);
6666
}
6767

68-
private Mono<DetectedEndpoint> detectEndpoint(Instance instance, EndpointDefinition endpoint) {
68+
protected Mono<DetectedEndpoint> detectEndpoint(Instance instance, EndpointDefinition endpoint) {
6969
URI uri = UriComponentsBuilder.fromUriString(instance.getRegistration().getManagementUrl())
7070
.path("/")
7171
.path(endpoint.getPath())
@@ -93,9 +93,9 @@ private Mono<DetectedEndpoint> detectEndpoint(Instance instance, EndpointDefinit
9393
});
9494
}
9595

96-
private Function<ClientResponse, Mono<DetectedEndpoint>> convert(InstanceId instanceId,
97-
EndpointDefinition endpointDefinition,
98-
URI uri) {
96+
protected Function<ClientResponse, Mono<DetectedEndpoint>> convert(InstanceId instanceId,
97+
EndpointDefinition endpointDefinition,
98+
URI uri) {
9999
return response -> {
100100
Mono<DetectedEndpoint> endpoint = Mono.empty();
101101
if (response.statusCode().is2xxSuccessful()) {
@@ -113,7 +113,7 @@ private Function<ClientResponse, Mono<DetectedEndpoint>> convert(InstanceId inst
113113
};
114114
}
115115

116-
private Mono<Endpoints> convert(List<DetectedEndpoint> endpoints) {
116+
protected Mono<Endpoints> convert(List<DetectedEndpoint> endpoints) {
117117
if (endpoints.isEmpty()) {
118118
return Mono.empty();
119119
}
@@ -135,7 +135,7 @@ private Mono<Endpoints> convert(List<DetectedEndpoint> endpoints) {
135135
}
136136

137137
@Data
138-
private static class DetectedEndpoint {
138+
protected static class DetectedEndpoint {
139139
private final EndpointDefinition definition;
140140
private final Endpoint endpoint;
141141

@@ -145,7 +145,7 @@ private static DetectedEndpoint of(EndpointDefinition endpointDefinition, String
145145
}
146146

147147
@Data
148-
private static class EndpointDefinition {
148+
protected static class EndpointDefinition {
149149
private final String id;
150150
private final String path;
151151

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/endpoints/QueryIndexEndpointStrategy.java

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import de.codecentric.boot.admin.server.domain.entities.Instance;
2020
import de.codecentric.boot.admin.server.domain.values.Endpoint;
2121
import de.codecentric.boot.admin.server.domain.values.Endpoints;
22+
import de.codecentric.boot.admin.server.domain.values.InstanceId;
2223
import de.codecentric.boot.admin.server.domain.values.Registration;
2324
import de.codecentric.boot.admin.server.web.client.InstanceWebClient;
2425
import lombok.Data;
@@ -62,12 +63,14 @@ public Mono<Endpoints> detectEndpoints(Instance instance) {
6263
.exchange()
6364
.flatMap(this.convert(instance, managementUrl))
6465
.onErrorResume(e -> {
65-
log.warn("Querying actuator-index for instance {} on '{}' failed: {}",
66+
log.warn(
67+
"Querying actuator-index for instance {} on '{}' failed: {}",
6668
instance.getId(),
6769
managementUrl,
6870
e.getMessage()
6971
);
70-
log.debug("Querying actuator-index for instance {} on '{}' failed.",
72+
log.debug(
73+
"Querying actuator-index for instance {} on '{}' failed.",
7174
instance.getId(),
7275
managementUrl,
7376
e
@@ -76,27 +79,56 @@ public Mono<Endpoints> detectEndpoints(Instance instance) {
7679
});
7780
}
7881

79-
private Function<ClientResponse, Mono<Endpoints>> convert(Instance instance, String managementUrl) {
82+
protected Function<ClientResponse, Mono<Endpoints>> convert(Instance instance, String managementUrl) {
8083
return response -> {
81-
if (response.statusCode().is2xxSuccessful() &&
82-
response.headers().contentType().map(actuatorMediaType::isCompatibleWith).orElse(false)) {
83-
log.debug("Querying actuator-index for instance {} on '{}' successful.",
84+
if (!response.statusCode().is2xxSuccessful()) {
85+
log.debug(
86+
"Querying actuator-index for instance {} on '{}' failed with status {}.",
8487
instance.getId(),
85-
managementUrl
88+
managementUrl,
89+
response.rawStatusCode()
8690
);
87-
return response.bodyToMono(Response.class).flatMap(this::convertResponse);
88-
} else {
89-
log.debug("Querying actuator-index for instance {} on '{}' failed with status {}.",
91+
return response.bodyToMono(Void.class).then(Mono.empty());
92+
}
93+
94+
if (!response.headers().contentType().map(actuatorMediaType::isCompatibleWith).orElse(false)) {
95+
log.debug(
96+
"Querying actuator-index for instance {} on '{}' failed with incompatible Content-Type '{}'.",
9097
instance.getId(),
9198
managementUrl,
92-
response.rawStatusCode()
99+
response.headers().contentType().map(Objects::toString).orElse("(missing)")
93100
);
94101
return response.bodyToMono(Void.class).then(Mono.empty());
95102
}
103+
104+
log.debug("Querying actuator-index for instance {} on '{}' successful.", instance.getId(), managementUrl);
105+
return response.bodyToMono(Response.class)
106+
.flatMap(this::convertResponse)
107+
.map(this.alignWithManagementUrl(instance.getId(), managementUrl));
108+
};
109+
}
110+
111+
protected Function<Endpoints, Endpoints> alignWithManagementUrl(InstanceId instanceId, String managementUrl) {
112+
return endpoints -> {
113+
if (!managementUrl.startsWith("https:")) {
114+
return endpoints;
115+
}
116+
if (endpoints.stream().noneMatch(e -> e.getUrl().startsWith("http:"))) {
117+
return endpoints;
118+
}
119+
log.warn(
120+
"Endpoints for instance {} queried from {} are falsely using http. Rewritten to https. Consider configuring this instance to use 'server.use-forward-headers=true'.",
121+
instanceId,
122+
managementUrl
123+
);
124+
125+
return Endpoints.of(endpoints.stream()
126+
.map(e -> Endpoint.of(e.getId(), e.getUrl().replaceFirst("http:", "https:")))
127+
.collect(Collectors.toList()));
96128
};
97129
}
98130

99-
private Mono<Endpoints> convertResponse(Response response) {
131+
protected Mono<Endpoints> convertResponse(Response response) {
100132
List<Endpoint> endpoints = response.getLinks()
101133
.entrySet()
102134
.stream()
@@ -107,12 +139,12 @@ private Mono<Endpoints> convertResponse(Response response) {
107139
}
108140

109141
@Data
110-
static class Response {
142+
protected static class Response {
111143
@JsonProperty("_links")
112144
private Map<String, EndpointRef> links = new HashMap<>();
113145

114146
@Data
115-
static class EndpointRef {
147+
protected static class EndpointRef {
116148
private final String href;
117149
private final boolean templated;
118150

spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/endpoints/QueryIndexEndpointStrategyTest.java

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,20 @@
2222
import de.codecentric.boot.admin.server.domain.values.InstanceId;
2323
import de.codecentric.boot.admin.server.domain.values.Registration;
2424
import de.codecentric.boot.admin.server.web.client.InstanceWebClient;
25+
import io.netty.channel.ChannelOption;
26+
import io.netty.handler.ssl.SslContextBuilder;
27+
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
28+
import io.netty.handler.timeout.ReadTimeoutHandler;
29+
import reactor.netty.ConnectionObserver;
30+
import reactor.netty.http.client.HttpClient;
2531
import reactor.test.StepVerifier;
2632

33+
import java.util.concurrent.TimeUnit;
2734
import org.junit.Rule;
2835
import org.junit.Test;
2936
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
3037
import org.springframework.http.MediaType;
31-
import com.github.tomakehurst.wiremock.core.Options;
38+
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
3239
import com.github.tomakehurst.wiremock.http.Fault;
3340
import com.github.tomakehurst.wiremock.junit.WireMockRule;
3441

@@ -39,15 +46,18 @@
3946
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
4047
import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
4148
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
49+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
4250
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
4351
import static java.util.Collections.singletonMap;
4452

4553
public class QueryIndexEndpointStrategyTest {
4654

4755
@Rule
48-
public WireMockRule wireMock = new WireMockRule(Options.DYNAMIC_PORT);
56+
public WireMockRule wireMock = new WireMockRule(wireMockConfig().dynamicHttpsPort());
4957

5058
private InstanceWebClient instanceWebClient = InstanceWebClient.builder()
59+
.webClientCustomizer(wc -> wc.clientConnector(
60+
httpConnector()))
5161
.retries(singletonMap(Endpoint.ACTUATOR_INDEX, 1))
5262
.build();
5363

@@ -59,7 +69,16 @@ public void should_return_endpoints() {
5969
.managementUrl(this.wireMock.url("/mgmt"))
6070
.build());
6171

62-
String body = "{\"_links\":{\"metrics-requiredMetricName\":{\"templated\":true,\"href\":\"\\/mgmt\\/metrics\\/{requiredMetricName}\"},\"self\":{\"templated\":false,\"href\":\"\\/mgmt\"},\"metrics\":{\"templated\":false,\"href\":\"\\/mgmt\\/stats\"},\"info\":{\"templated\":false,\"href\":\"\\/mgmt\\/info\"}}}";
72+
String host = "https://localhost:" + this.wireMock.httpsPort();
73+
String body = "{\"_links\":{\"metrics-requiredMetricName\":{\"templated\":true,\"href\":\"" +
74+
host +
75+
"/mgmt/metrics/{requiredMetricName}\"},\"self\":{\"templated\":false,\"href\":\"" +
76+
host +
77+
"/mgmt\"},\"metrics\":{\"templated\":false,\"href\":\"" +
78+
host +
79+
"/mgmt/stats\"},\"info\":{\"templated\":false,\"href\":\"" +
80+
host +
81+
"/mgmt/info\"}}}";
6382

6483
this.wireMock.stubFor(get("/mgmt").willReturn(ok(body).withHeader("Content-Type", ActuatorMediaType.V2_JSON)
6584
.withHeader("Content-Length",
@@ -71,7 +90,43 @@ public void should_return_endpoints() {
7190
//when
7291
StepVerifier.create(strategy.detectEndpoints(instance))
7392
//then
74-
.expectNext(Endpoints.single("metrics", "/mgmt/stats").withEndpoint("info", "/mgmt/info"))//
93+
.expectNext(Endpoints.single("metrics", host + "/mgmt/stats")
94+
.withEndpoint("info", host + "/mgmt/info"))//
95+
.verifyComplete();
96+
}
97+
98+
@Test
99+
public void should_return_endpoints_with_aligned_scheme() {
100+
//given
101+
Instance instance = Instance.create(InstanceId.of("id"))
102+
.register(Registration.create("test", this.wireMock.url("/mgmt/health"))
103+
.managementUrl(this.wireMock.url("/mgmt"))
104+
.build());
105+
106+
String host = "http://localhost:" + this.wireMock.httpsPort();
107+
String body = "{\"_links\":{\"metrics-requiredMetricName\":{\"templated\":true,\"href\":\"" +
108+
host +
109+
"/mgmt/metrics/{requiredMetricName}\"},\"self\":{\"templated\":false,\"href\":\"" +
110+
host +
111+
"/mgmt\"},\"metrics\":{\"templated\":false,\"href\":\"" +
112+
host +
113+
"/mgmt/stats\"},\"info\":{\"templated\":false,\"href\":\"" +
114+
host +
115+
"/mgmt/info\"}}}";
116+
117+
this.wireMock.stubFor(get("/mgmt").willReturn(ok(body).withHeader("Content-Type", ActuatorMediaType.V2_JSON)
118+
.withHeader("Content-Length",
119+
Integer.toString(body.length())
120+
)));
121+
122+
QueryIndexEndpointStrategy strategy = new QueryIndexEndpointStrategy(this.instanceWebClient);
123+
124+
//when
125+
String secureHost = "https://localhost:" + this.wireMock.httpsPort();
126+
StepVerifier.create(strategy.detectEndpoints(instance))
127+
//then
128+
.expectNext(Endpoints.single("metrics", secureHost + "/mgmt/stats")
129+
.withEndpoint("info", secureHost + "/mgmt/info"))//
75130
.verifyComplete();
76131
}
77132

@@ -180,7 +235,7 @@ public void should_retry() {
180235
.managementUrl(this.wireMock.url("/mgmt"))
181236
.build());
182237

183-
String body = "{\"_links\":{\"metrics-requiredMetricName\":{\"templated\":true,\"href\":\"\\/mgmt\\/metrics\\/{requiredMetricName}\"},\"self\":{\"templated\":false,\"href\":\"\\/mgmt\"},\"metrics\":{\"templated\":false,\"href\":\"\\/mgmt\\/stats\"},\"info\":{\"templated\":false,\"href\":\"\\/mgmt\\/info\"}}}";
238+
String body = "{\"_links\":{\"metrics-requiredMetricName\":{\"templated\":true,\"href\":\"/mgmt/metrics/{requiredMetricName}\"},\"self\":{\"templated\":false,\"href\":\"/mgmt\"},\"metrics\":{\"templated\":false,\"href\":\"/mgmt/stats\"},\"info\":{\"templated\":false,\"href\":\"/mgmt/info\"}}}";
184239

185240
this.wireMock.stubFor(get("/mgmt").inScenario("retry")
186241
.whenScenarioStateIs(STARTED)
@@ -202,4 +257,24 @@ public void should_retry() {
202257
.expectNext(Endpoints.single("metrics", "/mgmt/stats").withEndpoint("info", "/mgmt/info"))//
203258
.verifyComplete();
204259
}
260+
261+
private ReactorClientHttpConnector httpConnector() {
262+
SslContextBuilder sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE);
263+
264+
HttpClient client = HttpClient.create()
265+
.secure(ssl -> ssl.sslContext(sslCtx))
266+
.compress(true)
267+
.tcpConfiguration(tcp -> tcp.bootstrap(bootstrap -> bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
268+
2000
269+
))
270+
.observe((connection, newState) -> {
271+
if (ConnectionObserver.State.CONNECTED.equals(
272+
newState)) {
273+
connection.addHandlerLast(new ReadTimeoutHandler(2000L,
274+
TimeUnit.MILLISECONDS
275+
));
276+
}
277+
}));
278+
return new ReactorClientHttpConnector(client);
279+
}
205280
}

0 commit comments

Comments
 (0)