com.c4_soft.springaddons.rest.AbstractSpringAddonsWebClientSupport Maven / Gradle / Ivy
package com.c4_soft.springaddons.rest;
import java.net.URL;
import java.util.Map;
import java.util.Optional;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClient.Builder;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.AuthorizationProperties;
import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.ProxyProvider;
/**
* @author Jerome Wacongne chl4mp@c4-soft.com
*/
public abstract class AbstractSpringAddonsWebClientSupport {
private final ProxySupport proxySupport;
private final Map restClientProperties;
/**
* A {@link BearerProvider} to get the Bearer from the request security context
*/
private final BearerProvider forwardingBearerProvider;
public AbstractSpringAddonsWebClientSupport(
SystemProxyProperties systemProxyProperties,
SpringAddonsRestProperties addonsRestProperties,
BearerProvider forwardingBearerProvider) {
super();
this.proxySupport = new ProxySupport(systemProxyProperties, addonsRestProperties);
this.restClientProperties = addonsRestProperties.getClient();
this.forwardingBearerProvider = forwardingBearerProvider;
}
public WebClient.Builder client() {
final var clientBuilder = WebClient.builder();
httpConnector(proxySupport).ifPresent(clientBuilder::clientConnector);
return clientBuilder;
}
/**
* @param clientName key in "com.c4-soft.springaddons.rest.client" entries of {@link SpringAddonsRestProperties}
* @return A {@link WebClient} Builder pre-configured with a base-URI and (optionally) with a Bearer Authorization
*/
public WebClient.Builder client(String clientName) {
final var clientProps = Optional.ofNullable(restClientProperties.get(clientName)).orElseThrow(() -> new RestConfigurationNotFoundException(clientName));
final var clientBuilder = client();
clientProps.getBaseUrl().map(URL::toString).ifPresent(clientBuilder::baseUrl);
authorize(clientBuilder, clientProps.getAuthorization(), clientName);
return clientBuilder;
}
/**
* Uses the provided {@link WebClient} to proxy the httpServiceClass
*
* @param
* @param client
* @param httpServiceClass class of the #64;Service (with {@link HttpExchange} methods) to proxy with a {@link WebClient}
* @return a #64;Service proxy with a {@link WebClient}
*/
public T service(WebClient client, Class httpServiceClass) {
return HttpServiceProxyFactory.builderFor(WebClientAdapter.create(client)).build().createClient(httpServiceClass);
}
/**
* Builds a {@link WebClient} with just the provided spring-addons {@link SpringAddonsRestProperties} and uses it to proxy the
* httpServiceClass.
*
* @param
* @param httpServiceClass class of the #64;Service (with {@link HttpExchange} methods) to proxy with a {@link WebClient}
* @param clientName key in "rest" entries of spring-addons client properties
* @return a #64;Service proxy with a {@link WebClient}
*/
public T service(String clientName, Class httpServiceClass) {
return this.service(this.client(clientName).build(), httpServiceClass);
}
protected Optional httpConnector(ProxySupport proxySupport) {
return proxySupport.getHostname().map(proxyHost -> {
return new ReactorClientHttpConnector(
HttpClient.create().proxy(
proxy -> proxy.type(protocoleToProxyType(proxySupport.getProtocol())).host(proxyHost).port(proxySupport.getPort())
.username(proxySupport.getUsername()).password(username -> proxySupport.getPassword())
.nonProxyHosts(proxySupport.getNoProxy()).connectTimeoutMillis(proxySupport.getConnectTimeoutMillis())));
});
}
protected void authorize(Builder clientBuilder, AuthorizationProperties authProps, String clientName) {
if (authProps.getOauth2().isConfigured() && authProps.getBasic().isConfigured()) {
throw new RestMisconfigurationConfigurationException(
"REST authorization configuration for %s can be made for either OAuth2 or Basic, but not both at a time".formatted(clientName));
}
if (authProps.getOauth2().isConfigured()) {
oauth2(clientBuilder, authProps.getOauth2(), clientName);
}
if (authProps.getBasic().isConfigured()) {
basic(clientBuilder, authProps.getBasic(), clientName);
}
}
protected void oauth2(Builder clientBuilder, AuthorizationProperties.OAuth2Properties oauth2Props, String clientName) {
if (!oauth2Props.isConfValid()) {
throw new RestMisconfigurationConfigurationException(
"REST OAuth2 authorization configuration for %s can be made for either a registration-id or resource server Bearer forwarding, but not both at a time"
.formatted(clientName));
}
oauth2Props.getOauth2RegistrationId().map(this::oauth2RegistrationFilter).ifPresent(clientBuilder::filter);
if (oauth2Props.isForwardBearer()) {
clientBuilder.filter((ClientRequest request, ExchangeFunction next) -> {
final var bearer = forwardingBearerProvider.getBearer();
if (bearer.isEmpty()) {
return next.exchange(request);
}
final var modified = ClientRequest.from(request);
modified.headers(headers -> headers.setBearerAuth(bearer.get()));
return next.exchange(modified.build());
});
}
}
protected abstract ExchangeFilterFunction oauth2RegistrationFilter(String registrationId);
protected void basic(Builder clientBuilder, AuthorizationProperties.BasicAuthProperties authProps, String clientName) {
if (authProps.getEncodedCredentials().isPresent()) {
if (authProps.getUsername().isPresent() || authProps.getPassword().isPresent() || authProps.getCharset().isPresent()) {
throw new RestMisconfigurationConfigurationException(
"REST Basic authorization for %s is misconfigured: when encoded-credentials is provided, username, password and charset must be absent."
.formatted(clientName));
}
} else {
if (authProps.getUsername().isEmpty() || authProps.getPassword().isEmpty()) {
throw new RestMisconfigurationConfigurationException(
"REST Basic authorization for %s is misconfigured: when encoded-credentials is empty, username & password are required."
.formatted(clientName));
}
}
clientBuilder.filter((ClientRequest request, ExchangeFunction next) -> {
if (authProps.getEncodedCredentials().isEmpty() && authProps.getUsername().isEmpty()) {
return next.exchange(request);
}
final var modified = ClientRequest.from(request);
if (authProps.getEncodedCredentials().isPresent()) {
modified.headers(headers -> headers.setBasicAuth(authProps.getEncodedCredentials().get()));
} else if (authProps.getCharset().isPresent()) {
modified.headers(headers -> headers.setBasicAuth(authProps.getUsername().get(), authProps.getPassword().get(), authProps.getCharset().get()));
} else {
modified.headers(headers -> headers.setBasicAuth(authProps.getUsername().get(), authProps.getPassword().get()));
}
return next.exchange(modified.build());
});
}
static ProxyProvider.Proxy protocoleToProxyType(String protocol) {
if (protocol == null) {
return null;
}
final var lower = protocol.toLowerCase();
if (lower.startsWith("http")) {
return ProxyProvider.Proxy.HTTP;
}
if (lower.startsWith("socks4")) {
return ProxyProvider.Proxy.SOCKS4;
}
return ProxyProvider.Proxy.SOCKS5;
}
}