feat(http): ApacheOutboundHttpClientFactory with memoization and startup validation
Adds ApacheOutboundHttpClientFactory (Apache HttpClient 5) that memoizes CloseableHttpClient instances keyed on effective TLS + timeout config, and OutboundHttpConfig (@ConfigurationProperties) that validates trusted CA paths at startup and exposes OutboundHttpClientFactory as a Spring bean. TRUST_ALL mode disables both cert validation (TrustAllManager in SslContextBuilder) and hostname verification (NoopHostnameVerifier on SSLConnectionSocketFactoryBuilder). WireMock HTTPS integration test covers trust-all bypass, system-default PKIX rejection, and client memoization. OIDC audit: OidcProviderHelper and OidcTokenExchanger use Nimbus SDK's own HTTP layer (DefaultResourceRetriever for JWKS, HTTPRequest.send() for token exchange) plus the bespoke InsecureTlsHelper for TLS skip-verify; neither uses OutboundHttpClientFactory. Retrofit deferred to a separate follow-up per plan §20. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package com.cameleer.server.app.http;
|
||||
|
||||
import com.cameleer.server.core.http.OutboundHttpProperties;
|
||||
import com.cameleer.server.core.http.OutboundHttpRequestContext;
|
||||
import com.cameleer.server.core.http.TrustMode;
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class ApacheOutboundHttpClientFactoryIT {
|
||||
|
||||
private WireMockServer wm;
|
||||
private ApacheOutboundHttpClientFactory factory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
wm = new WireMockServer(WireMockConfiguration.options()
|
||||
.httpDisabled(true).dynamicHttpsPort());
|
||||
wm.start();
|
||||
wm.stubFor(get("/ping").willReturn(ok("pong")));
|
||||
|
||||
OutboundHttpProperties props = new OutboundHttpProperties(
|
||||
false, List.of(), Duration.ofSeconds(2), Duration.ofSeconds(5), null, null, null);
|
||||
factory = new ApacheOutboundHttpClientFactory(props, new SslContextBuilder());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
wm.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void trustAllBypassesWiremockSelfSignedCert() throws Exception {
|
||||
CloseableHttpClient client = factory.clientFor(
|
||||
new OutboundHttpRequestContext(TrustMode.TRUST_ALL, List.of(), null, null));
|
||||
try (var resp = client.execute(new HttpGet("https://localhost:" + wm.httpsPort() + "/ping"))) {
|
||||
assertThat(resp.getCode()).isEqualTo(200);
|
||||
assertThat(EntityUtils.toString(resp.getEntity())).isEqualTo("pong");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void systemDefaultRejectsSelfSignedCert() {
|
||||
CloseableHttpClient client = factory.clientFor(OutboundHttpRequestContext.systemDefault());
|
||||
assertThatThrownBy(() -> client.execute(new HttpGet("https://localhost:" + wm.httpsPort() + "/ping")))
|
||||
.hasMessageContaining("PKIX");
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientsAreMemoizedByContext() {
|
||||
CloseableHttpClient c1 = factory.clientFor(OutboundHttpRequestContext.systemDefault());
|
||||
CloseableHttpClient c2 = factory.clientFor(OutboundHttpRequestContext.systemDefault());
|
||||
assertThat(c1).isSameAs(c2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user