Skip to content

Commit

Permalink
WiP ClientHttpRequestFactory auto-configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
ch4mpy committed Nov 1, 2024
1 parent 7fa91b4 commit 879617a
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientHttpRequestFactoryProperties.ProxyProperties;
import lombok.RequiredArgsConstructor;

/**
Expand All @@ -18,73 +19,68 @@
@RequiredArgsConstructor
public class ProxySupport {
private final SystemProxyProperties systemProxyProperties;
private final SpringAddonsRestProperties restProperties;
private final ProxyProperties springAddonsProperties;

public boolean isEnabled() {
return restProperties.getProxy().isEnabled() && getHostname().isPresent();
return springAddonsProperties.isEnabled() && getHostname().isPresent();
}

public Optional<String> getHostname() {
if (!restProperties.getProxy().isEnabled()) {
if (!springAddonsProperties.isEnabled()) {
return Optional.empty();
}
return restProperties.getProxy().getHost()
return springAddonsProperties.getHost()
.or(() -> systemProxyProperties.getHttpProxy().map(URL::getHost));
}

public String getProtocol() {
if (!restProperties.getProxy().isEnabled()) {
if (!springAddonsProperties.isEnabled()) {
return null;
}
return restProperties.getProxy().getHost().map(h -> restProperties.getProxy().getProtocol())
return springAddonsProperties.getHost().map(h -> springAddonsProperties.getProtocol())
.orElse(systemProxyProperties.getHttpProxy().map(URL::getProtocol).orElse(null));
}

public int getPort() {
return restProperties.getProxy().getHost().map(h -> restProperties.getProxy().getPort())
.orElse(systemProxyProperties.getHttpProxy().map(URL::getPort)
.orElse(restProperties.getProxy().getPort()));
return springAddonsProperties.getHost().map(h -> springAddonsProperties.getPort()).orElse(
systemProxyProperties.getHttpProxy().map(URL::getPort).orElse(springAddonsProperties.getPort()));
}

public String getUsername() {
if (!restProperties.getProxy().isEnabled()) {
if (!springAddonsProperties.isEnabled()) {
return null;
}
return restProperties.getProxy().getHost().map(h -> restProperties.getProxy().getUsername())
return springAddonsProperties.getHost().map(h -> springAddonsProperties.getUsername())
.orElse(systemProxyProperties.getHttpProxy().map(URL::getUserInfo)
.map(ProxySupport::getUserinfoName).orElse(null));
}

public String getPassword() {
if (!restProperties.getProxy().isEnabled()) {
if (!springAddonsProperties.isEnabled()) {
return null;
}
return restProperties.getProxy().getHost().map(h -> restProperties.getProxy().getPassword())
return springAddonsProperties.getHost().map(h -> springAddonsProperties.getPassword())
.orElse(systemProxyProperties.getHttpProxy().map(URL::getUserInfo)
.map(ProxySupport::getUserinfoPassword).orElse(null));
}

public String getNoProxy() {
if (!restProperties.getProxy().isEnabled()) {
if (!springAddonsProperties.isEnabled()) {
return null;
}
return Optional.ofNullable(restProperties.getProxy().getNonProxyHostsPattern())
return Optional.ofNullable(springAddonsProperties.getNonProxyHostsPattern())
.filter(StringUtils::hasText)
.orElse(getNonProxyHostsPattern(systemProxyProperties.getNoProxy()));
}

public int getConnectTimeoutMillis() {
return restProperties.getProxy().getConnectTimeoutMillis();
return springAddonsProperties.getConnectTimeoutMillis();
}

public SystemProxyProperties getSystemProperties() {
return systemProxyProperties;
}

public SpringAddonsRestProperties.ProxyProperties getAddonsProperties() {
return restProperties.getProxy();
}

static String getUserinfoName(String userinfo) {
if (userinfo == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.c4_soft.springaddons.rest;

import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
Expand All @@ -20,16 +23,19 @@
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.AuthorizationProperties;
import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientHttpRequestFactoryProperties;
import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientHttpRequestFactoryProperties.ProxyProperties;
import com.c4_soft.springaddons.rest.SpringAddonsRestProperties.RestClientProperties.ClientType;
import lombok.Data;
import lombok.Setter;
Expand Down Expand Up @@ -166,13 +172,11 @@ public static class RestClientBuilderFactoryBean implements FactoryBean<RestClie
@Override
@Nullable
public RestClient.Builder getObject() throws Exception {
final var builder = RestClient.builder();
final var clientProps = Optional.ofNullable(restProperties.getClient().get(clientId))
.orElseThrow(() -> new RestConfigurationNotFoundException(clientId));

if (!clientProps.isIgnoreHttpProxy()) {
configureProxy(builder, systemProxyProperties, restProperties);
}
final var builder = configureClientHttpRequestFactory(RestClient.builder(),
systemProxyProperties, clientProps.getHttp());

clientProps.getBaseUrl().map(URL::toString).ifPresent(builder::baseUrl);

Expand All @@ -187,19 +191,55 @@ public Class<?> getObjectType() {
return RestClient.Builder.class;
}

public static RestClient.Builder configureClientHttpRequestFactory(RestClient.Builder builder,
SimpleClientHttpRequestFactory requestFactory, SystemProxyProperties systemProxyProperties,
ClientHttpRequestFactoryProperties springAddonsProperties) {
final var proxySupport =
new ProxySupport(systemProxyProperties, springAddonsProperties.getProxy());

proxySupport.getHostname().ifPresent(proxyHostname -> {
final var address = new InetSocketAddress(proxyHostname, proxySupport.getPort());
requestFactory.setProxy(new Proxy(protocolToProxyType(proxySupport.getProtocol()), address));

if (StringUtils.hasText(proxySupport.getUsername())
&& StringUtils.hasText(proxySupport.getPassword())) {
final var base64 = Base64.getEncoder()
.encodeToString((proxySupport.getUsername() + ':' + proxySupport.getPassword())
.getBytes(StandardCharsets.UTF_8));
builder.defaultHeader(HttpHeaders.PROXY_AUTHORIZATION, "Basic %s".formatted(base64));
}

Optional.ofNullable(proxySupport.getNoProxy()).map(Pattern::compile)
});

return builder;
}

static Proxy.Type protocolToProxyType(String protocol) {
if (protocol == null) {
return null;
}
final var lower = protocol.toLowerCase();
if (lower.startsWith("http")) {
return Proxy.Type.HTTP;
}
if (lower.startsWith("socks")) {
return Proxy.Type.SOCKS;
}
return null;
}

public static RestClient.Builder configureProxy(RestClient.Builder builder,
SystemProxyProperties systemProxyProperties, SpringAddonsRestProperties restProperties) {
final var proxySupport = new ProxySupport(systemProxyProperties, restProperties);
SystemProxyProperties systemProxyProperties, ProxyProperties springAddonsProxyProperties) {
final var proxySupport = new ProxySupport(systemProxyProperties, springAddonsProxyProperties);
proxySupport.getHostname()
.map(proxyHostname -> new SpringAddonsClientHttpRequestFactory(proxySupport))
.ifPresent(builder::requestFactory);
if (proxySupport.getAddonsProperties().isEnabled()
&& StringUtils.hasText(proxySupport.getAddonsProperties().getUsername())
&& StringUtils.hasText(proxySupport.getAddonsProperties().getPassword())) {
if (StringUtils.hasText(proxySupport.getUsername())
&& StringUtils.hasText(proxySupport.getPassword())) {
final var base64 = Base64.getEncoder()
.encodeToString((proxySupport.getAddonsProperties().getUsername() + ':'
+ proxySupport.getAddonsProperties().getPassword())
.getBytes(StandardCharsets.UTF_8));
.encodeToString((proxySupport.getUsername() + ':' + proxySupport.getPassword())
.getBytes(StandardCharsets.UTF_8));
builder.defaultHeader(HttpHeaders.PROXY_AUTHORIZATION, "Basic %s".formatted(base64));
}
return builder;
Expand Down Expand Up @@ -237,7 +277,7 @@ protected void setBearerAuthorizationHeader(RestClient.Builder clientBuilder,
protected ClientHttpRequestInterceptor forwardingClientHttpRequestInterceptor() {
return (HttpRequest request, byte[] body, ClientHttpRequestExecution execution) -> {
final var auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof AbstractOAuth2Token oauth2Token) {
if (auth != null && auth.getPrincipal() instanceof OAuth2Token oauth2Token) {
request.getHeaders().setBearerAuth(oauth2Token.getTokenValue());
}
return execution.execute(request, body);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
Expand All @@ -24,26 +25,22 @@
@AutoConfiguration
@ConfigurationProperties(prefix = "com.c4-soft.springaddons.rest")
public class SpringAddonsRestProperties {
/**
* <p>
* Configure Proxy-Authorization header for authentication on a HTTP or SOCKS proxy. This header
* auto-configuration can be disable on each client.
* </p>
* <p>
* HTTP_PROXY and NO_PROXY standard environment variable are used only if
* "com.c4-soft.springaddons.rest.proxy.hostname" is left empty and
* "com.c4-soft.springaddons.rest.proxy.enabled" is TRUE or null. In other words, if the standard
* environment variables are correctly set, leaving "hostname" and "enabled" empty in
* "springaddons" properties is probably the best option.
* </p>
*/
private ProxyProperties proxy = new ProxyProperties();

/**
* Expose {@link RestClient} or {@link WebClient} instances as named beans
*/
private Map<String, RestClientProperties> client = new HashMap<>();

// FIXME: enable when a way is found to generate and register service proxies as beans.
// For instance, have the HttpExchangeProxyFactoryBean definitions registered with a
// BeanDefinitionRegistryPostProcessor

// /**
// * Expose {@link HttpExchange &#64;HttpExchange} proxies as named beans (generated using
// * {@link HttpServiceProxyFactory})
// */
// private Map<String, RestServiceProperties> service = new HashMap<>();

public String getClientBeanName(String clientId) {
if (!client.containsKey(clientId)) {
return null;
Expand Down Expand Up @@ -76,30 +73,6 @@ private static String toCamelCase(String in) {
return builder.toString();
}

// FIXME: enable when a way is found to generate and register service proxies as beans.
// For instance, have the HttpExchangeProxyFactoryBean definitions registered with a
// BeanDefinitionRegistryPostProcessor

// /**
// * Expose {@link HttpExchange &#64;HttpExchange} proxies as named beans (generated using
// * {@link HttpServiceProxyFactory})
// */
// private Map<String, RestServiceProperties> service = new HashMap<>();

@Data
public static class ProxyProperties {
private boolean enabled = true;
private String protocol = "http";
private int port = 8080;
private String username;
private String password;
private int connectTimeoutMillis = 10000;

private Optional<String> host = Optional.empty();

private String nonProxyHostsPattern;
}

@Data
public static class RestClientProperties {
/**
Expand All @@ -114,16 +87,16 @@ public static class RestClientProperties {
private AuthorizationProperties authorization = new AuthorizationProperties();

/**
* Defines the type of the REST client. Default is {@link RestClient} in servlet applications
* and {@link WebClient} in reactive ones.
* Configure the internal {@link SimpleClientHttpRequestFactory} with timeouts and HTTP or SOCKS
* proxy
*/
private ClientType type = ClientType.DEFAULT;
private ClientHttpRequestFactoryProperties http = new ClientHttpRequestFactoryProperties();

/**
* If true, the Proxy-Authorization header is not automatically added to the requests of this
* REST client.
* Defines the type of the REST client. Default is {@link RestClient} in servlet applications
* and {@link WebClient} in reactive ones.
*/
private boolean ignoreHttpProxy = false;
private ClientType type = ClientType.DEFAULT;

/**
* If true, what is exposed as a bean is the pre-configured {@link RestClient.Builder} or
Expand Down Expand Up @@ -225,6 +198,49 @@ boolean isConfValid() {
}
}

@Data
public static class ClientHttpRequestFactoryProperties {
/**
* <p>
* Configure Proxy-Authorization header for authentication on a HTTP or SOCKS proxy. This
* header auto-configuration can be disable on each client.
* </p>
* <p>
* HTTP_PROXY and NO_PROXY standard environment variable are used only if
* "com.c4-soft.springaddons.rest.proxy.hostname" is left empty and
* "com.c4-soft.springaddons.rest.proxy.enabled" is TRUE or null. In other words, if the
* standard environment variables are correctly set, leaving "proxy" properties empty here is
* probably the best option.
* </p>
*/
private ProxyProperties proxy = new ProxyProperties();

/**
* Connection timeout in milliseconds.
*/
private Optional<Integer> connectTimeoutMillis = Optional.empty();

/**
* Read timeout in milliseconds.
*/
private Optional<Integer> readTimeoutMillis = Optional.empty();

@Data
public static class ProxyProperties {
private boolean enabled = true;
private String protocol = "http";
private int port = 8080;
private String username;
private String password;
private int connectTimeoutMillis = 10000;

private Optional<String> host = Optional.empty();

private String nonProxyHostsPattern;
}

}

public static enum ClientType {
DEFAULT, REST_CLIENT, WEB_CLIENT;
}
Expand Down

0 comments on commit 879617a

Please sign in to comment.