diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a411d..e208892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.0.1 - 2024-11-06] +## [0.0.1 - 2024-11-08] ### Added - First version of the project - Spring Application @@ -147,3 +147,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Log bridgeheads sorted - Replace hyphen in frontend sites - Human Readable Bridgehead for frontend dto +- Http proxy configuration diff --git a/src/main/java/de/samply/app/ProjectManagerConst.java b/src/main/java/de/samply/app/ProjectManagerConst.java index eff4294..443aa58 100644 --- a/src/main/java/de/samply/app/ProjectManagerConst.java +++ b/src/main/java/de/samply/app/ProjectManagerConst.java @@ -233,6 +233,8 @@ public class ProjectManagerConst { public final static String JWKS_URI_PROPERTY = "spring.security.oauth2.client.provider.oidc.jwk-set-uri"; public final static String REGISTERED_BRIDGEHEADS = "bridgeheads"; public final static String FRONTEND_CONFIG = "frontend"; + public final static String HTTP_PROXY_PREFIX = "http.proxy"; + public final static String HTTPS_PROXY_PREFIX = "https.proxy"; // Exporter public final static String SECURITY_ENABLED = "SECURITY_ENABLED"; @@ -461,6 +463,8 @@ public class ProjectManagerConst { public final static String CUSTOM_PROJECT_CONFIGURATION = "CUSTOM"; public final static String EMAIL_SERVICE = "EMAIL_SERVICE"; public final static String HYPHEN = "minus"; + public final static String HTTP_PROTOCOL_SCHEMA = "http"; + public final static String HTTPS_PROTOCOL_SCHEMA = "https"; } diff --git a/src/main/java/de/samply/proxy/HttpProxyConfiguration.java b/src/main/java/de/samply/proxy/HttpProxyConfiguration.java new file mode 100644 index 0000000..d325d09 --- /dev/null +++ b/src/main/java/de/samply/proxy/HttpProxyConfiguration.java @@ -0,0 +1,15 @@ +package de.samply.proxy; + +import de.samply.app.ProjectManagerConst; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = ProjectManagerConst.HTTP_PROXY_PREFIX) +public class HttpProxyConfiguration extends ProxyConfiguration { + + public HttpProxyConfiguration() { + this.setSchema(ProjectManagerConst.HTTP_PROTOCOL_SCHEMA); + } + +} diff --git a/src/main/java/de/samply/proxy/HttpsProxyConfiguration.java b/src/main/java/de/samply/proxy/HttpsProxyConfiguration.java new file mode 100644 index 0000000..fc544d5 --- /dev/null +++ b/src/main/java/de/samply/proxy/HttpsProxyConfiguration.java @@ -0,0 +1,15 @@ +package de.samply.proxy; + +import de.samply.app.ProjectManagerConst; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = ProjectManagerConst.HTTPS_PROXY_PREFIX) +public class HttpsProxyConfiguration extends ProxyConfiguration { + + public HttpsProxyConfiguration() { + this.setSchema(ProjectManagerConst.HTTPS_PROTOCOL_SCHEMA); + } + +} diff --git a/src/main/java/de/samply/proxy/ProxyConfiguration.java b/src/main/java/de/samply/proxy/ProxyConfiguration.java new file mode 100644 index 0000000..a39621a --- /dev/null +++ b/src/main/java/de/samply/proxy/ProxyConfiguration.java @@ -0,0 +1,85 @@ +package de.samply.proxy; + +import jakarta.annotation.PostConstruct; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Data +public class ProxyConfiguration { + + private String host; + private Integer port; + private String schema; + private String username; + private String password; + private String url; + + // Comma-separated list of hosts or IPs to bypass the proxy + private String noProxy; + private String noProxyPattern; + + // Convenience method to get noProxy as a List + public List getNoProxyList() { + return noProxy != null ? Arrays.asList(noProxy.split(",")) : List.of(); + } + + @PostConstruct + public void init() throws MalformedURLException { + if (StringUtils.hasText(this.url)) { + URL url = new URL(this.url); + this.schema = url.getProtocol(); + this.host = url.getHost(); + this.port = url.getPort(); + setProxyUserAndPassword(url.getUserInfo()); + if (this.noProxy != null) { + this.noProxyPattern = createNonProxyPattern(this.noProxy); + } + } + if (isConfigured()) { + String schema = (this.schema != null) ? this.schema : ""; + log.info(schema + " Proxy configured:"); + log.info("\t-Host: " + this.host); + log.info("\t-Port: " + this.port); + if (username != null && password != null) { + log.info("\t-Username: " + username); + } + if (noProxy != null) { + log.info("\t-NoProxy: " + noProxy); + } + } + } + + private void setProxyUserAndPassword(String userInfo) { + if (userInfo != null) { + String[] credentials = userInfo.split(":"); + if (credentials.length == 2) { + this.username = credentials[0]; + this.password = credentials[1]; + } + } + } + + public boolean isConfigured() { + return StringUtils.hasText(this.host) && this.port != null; + } + + // Method to create a regex pattern for non-proxy hosts + private static String createNonProxyPattern(String nonProxyHosts) { + // Split the comma-separated list into individual host patterns and build the regex + return Arrays.stream(nonProxyHosts.split(",")) + .map(String::trim) // Trim whitespace + .map(host -> host.replace(".", "\\.") // Escape dots to literal + .replace("*", ".*")) // Convert '*' to '.*' for wildcard matching + .collect(Collectors.joining("|")); // Join with '|' to match any pattern + } + + +} diff --git a/src/main/java/de/samply/utils/WebClientFactory.java b/src/main/java/de/samply/utils/WebClientFactory.java index 3bc8d0e..db36ae5 100644 --- a/src/main/java/de/samply/utils/WebClientFactory.java +++ b/src/main/java/de/samply/utils/WebClientFactory.java @@ -1,6 +1,9 @@ package de.samply.utils; import de.samply.app.ProjectManagerConst; +import de.samply.proxy.HttpProxyConfiguration; +import de.samply.proxy.HttpsProxyConfiguration; +import de.samply.proxy.ProxyConfiguration; import io.netty.channel.ChannelOption; import io.netty.channel.epoll.EpollChannelOption; import org.springframework.beans.factory.annotation.Value; @@ -8,8 +11,10 @@ import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import reactor.netty.http.client.HttpClient; +import reactor.netty.transport.ProxyProvider; import java.time.Duration; +import java.util.Optional; @Component public class WebClientFactory { @@ -22,6 +27,8 @@ public class WebClientFactory { private final int webClientTcpKeepIntervalInSeconds; private final int webClientTcpKeepConnetionNumberOfTries; private final int webClientBufferSizeInBytes; + private Optional httpProxyConfiguration = Optional.empty(); + private Optional httpsProxyConfiguration = Optional.empty(); public WebClientFactory( @Value(ProjectManagerConst.WEBCLIENT_REQUEST_TIMEOUT_IN_SECONDS_SV) Integer webClientRequestTimeoutInSeconds, @@ -31,7 +38,9 @@ public WebClientFactory( @Value(ProjectManagerConst.WEBCLIENT_TCP_KEEP_CONNECTION_NUMBER_OF_TRIES_SV) Integer webClientTcpKeepConnetionNumberOfTries, @Value(ProjectManagerConst.WEBCLIENT_MAX_NUMBER_OF_RETRIES_SV) Integer webClientMaxNumberOfRetries, @Value(ProjectManagerConst.WEBCLIENT_TIME_IN_SECONDS_AFTER_RETRY_WITH_FAILURE_SV) Integer webClientTimeInSecondsAfterRetryWithFailure, - @Value(ProjectManagerConst.WEBCLIENT_BUFFER_SIZE_IN_BYTES_SV) Integer webClientBufferSizeInBytes + @Value(ProjectManagerConst.WEBCLIENT_BUFFER_SIZE_IN_BYTES_SV) Integer webClientBufferSizeInBytes, + HttpProxyConfiguration httpProxyConfiguration, + HttpsProxyConfiguration httpsProxyConfiguration ) { this.webClientMaxNumberOfRetries = webClientMaxNumberOfRetries; this.webClientTimeInSecondsAfterRetryWithFailure = webClientTimeInSecondsAfterRetryWithFailure; @@ -41,10 +50,21 @@ public WebClientFactory( this.webClientTcpKeepIntervalInSeconds = webClientTcpKeepIntervalInSeconds; this.webClientTcpKeepConnetionNumberOfTries = webClientTcpKeepConnetionNumberOfTries; this.webClientBufferSizeInBytes = webClientBufferSizeInBytes; + + setHttpProxies(httpProxyConfiguration, httpsProxyConfiguration); + } + + private void setHttpProxies(HttpProxyConfiguration httpProxyConfiguration, HttpsProxyConfiguration httpsProxyConfiguration) { + if (httpProxyConfiguration.isConfigured()) { + this.httpProxyConfiguration = Optional.of(httpProxyConfiguration); + } + if (httpsProxyConfiguration.isConfigured()) { + this.httpsProxyConfiguration = Optional.of(httpsProxyConfiguration); + } } public WebClient createWebClient(String baseUrl) { - return WebClient.builder() + WebClient.Builder webClientBuilder = WebClient.builder() .codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(webClientBufferSizeInBytes)) .clientConnector(new ReactorClientHttpConnector( HttpClient.create() @@ -55,9 +75,22 @@ public WebClient createWebClient(String baseUrl) { .option(EpollChannelOption.TCP_KEEPINTVL, webClientTcpKeepIntervalInSeconds) .option(EpollChannelOption.TCP_KEEPCNT, webClientTcpKeepConnetionNumberOfTries) )) - .baseUrl(baseUrl).build(); + .baseUrl(baseUrl); + httpsProxyConfiguration.ifPresent(proxyConfig -> addProxy(webClientBuilder, proxyConfig)); + httpProxyConfiguration.ifPresent(proxyConfig -> addProxy(webClientBuilder, proxyConfig)); + return webClientBuilder.build(); } + private void addProxy(WebClient.Builder webClientBuilder, ProxyConfiguration proxyConfiguration) { + webClientBuilder.clientConnector(new ReactorClientHttpConnector(HttpClient.create() + .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP) + .host(proxyConfiguration.getHost()) + .port(proxyConfiguration.getPort()) + .username(proxyConfiguration.getUsername()) + .password(password -> proxyConfiguration.getPassword()) // Use Function.identity() here if needed + .nonProxyHosts(proxyConfiguration.getNoProxyPattern()) + ))); + } public int getWebClientMaxNumberOfRetries() { return webClientMaxNumberOfRetries;