diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java index 6a9f5e7158e..dada651f098 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java @@ -100,8 +100,9 @@ public class JvmMetricsAutoConfiguration { @ConditionalOnMissingBean(type = VIRTUAL_THREAD_METRICS_CLASS) @ImportRuntimeHints(VirtualThreadMetricsRuntimeHintsRegistrar.class) MeterBinder virtualThreadMetrics() throws ClassNotFoundException { - Class clazz = ClassUtils.forName(VIRTUAL_THREAD_METRICS_CLASS, getClass().getClassLoader()); - return (MeterBinder) BeanUtils.instantiateClass(clazz); + Class virtualThreadMetricsClass = ClassUtils.forName(VIRTUAL_THREAD_METRICS_CLASS, + getClass().getClassLoader()); + return (MeterBinder) BeanUtils.instantiateClass(virtualThreadMetricsClass); } static final class VirtualThreadMetricsRuntimeHintsRegistrar implements RuntimeHintsRegistrar { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/PropertiesRedisConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/PropertiesRedisConnectionDetails.java index 1efebda45e8..f7a836f23dc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/PropertiesRedisConnectionDetails.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/PropertiesRedisConnectionDetails.java @@ -18,8 +18,6 @@ package org.springframework.boot.autoconfigure.data.redis; import java.util.List; -import org.springframework.boot.autoconfigure.data.redis.RedisConnectionConfiguration.ConnectionInfo; - /** * Adapts {@link RedisProperties} to {@link RedisConnectionDetails}. * @@ -28,6 +26,7 @@ import org.springframework.boot.autoconfigure.data.redis.RedisConnectionConfigur * @author Phillip Webb * @author Scott Frederick * @author Yanming Zhou + * @author Phillip Webb */ class PropertiesRedisConnectionDetails implements RedisConnectionDetails { @@ -39,76 +38,45 @@ class PropertiesRedisConnectionDetails implements RedisConnectionDetails { @Override public String getUsername() { - if (this.properties.getUrl() != null) { - ConnectionInfo connectionInfo = ConnectionInfo.of(this.properties.getUrl()); - return connectionInfo.getUsername(); - } - return this.properties.getUsername(); + RedisUrl redisUrl = getRedisUrl(); + return (redisUrl != null) ? redisUrl.credentials().username() : this.properties.getUsername(); } @Override public String getPassword() { - if (this.properties.getUrl() != null) { - ConnectionInfo connectionInfo = ConnectionInfo.of(this.properties.getUrl()); - return connectionInfo.getPassword(); - } - return this.properties.getPassword(); + RedisUrl redisUrl = getRedisUrl(); + return (redisUrl != null) ? redisUrl.credentials().password() : this.properties.getPassword(); } @Override public Standalone getStandalone() { - if (this.properties.getUrl() != null) { - ConnectionInfo connectionInfo = ConnectionInfo.of(this.properties.getUrl()); - return Standalone.of(connectionInfo.getUri().getHost(), connectionInfo.getUri().getPort(), - connectionInfo.getDatabase()); - } - return Standalone.of(this.properties.getHost(), this.properties.getPort(), this.properties.getDatabase()); + RedisUrl redisUrl = getRedisUrl(); + return (redisUrl != null) + ? Standalone.of(redisUrl.uri().getHost(), redisUrl.uri().getPort(), redisUrl.database()) + : Standalone.of(this.properties.getHost(), this.properties.getPort(), this.properties.getDatabase()); } @Override public Sentinel getSentinel() { - org.springframework.boot.autoconfigure.data.redis.RedisProperties.Sentinel sentinel = this.properties - .getSentinel(); - if (sentinel == null) { - return null; - } - return new Sentinel() { - - @Override - public int getDatabase() { - return getStandalone().getDatabase(); - } - - @Override - public String getMaster() { - return sentinel.getMaster(); - } - - @Override - public List getNodes() { - return sentinel.getNodes().stream().map(PropertiesRedisConnectionDetails.this::asNode).toList(); - } - - @Override - public String getUsername() { - return sentinel.getUsername(); - } - - @Override - public String getPassword() { - return sentinel.getPassword(); - } - - }; + RedisProperties.Sentinel sentinel = this.properties.getSentinel(); + return (sentinel != null) ? new PropertiesSentinel(getStandalone().getDatabase(), sentinel) : null; } @Override public Cluster getCluster() { RedisProperties.Cluster cluster = this.properties.getCluster(); - List nodes = (cluster != null) ? cluster.getNodes().stream().map(this::asNode).toList() : null; + List nodes = (cluster != null) ? asNodes(cluster.getNodes()) : null; return (nodes != null) ? () -> nodes : null; } + private RedisUrl getRedisUrl() { + return RedisUrl.of(this.properties.getUrl()); + } + + private List asNodes(List nodes) { + return nodes.stream().map(this::asNode).toList(); + } + private Node asNode(String node) { int portSeparatorIndex = node.lastIndexOf(':'); String host = node.substring(0, portSeparatorIndex); @@ -116,4 +84,45 @@ class PropertiesRedisConnectionDetails implements RedisConnectionDetails { return new Node(host, port); } + /** + * {@link Sentinel} implementation backed by properties. + */ + private class PropertiesSentinel implements Sentinel { + + private final int database; + + private final RedisProperties.Sentinel properties; + + PropertiesSentinel(int database, RedisProperties.Sentinel properties) { + this.database = database; + this.properties = properties; + } + + @Override + public int getDatabase() { + return this.database; + } + + @Override + public String getMaster() { + return this.properties.getMaster(); + } + + @Override + public List getNodes() { + return asNodes(this.properties.getNodes()); + } + + @Override + public String getUsername() { + return this.properties.getUsername(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java index f24e4aa74ff..4163ebc8ebb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.boot.autoconfigure.data.redis; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; @@ -33,7 +31,6 @@ import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; /** * Base Redis connection configuration. @@ -152,14 +149,18 @@ abstract class RedisConnectionConfiguration { return this.properties; } - protected SslBundles getSslBundles() { + protected final SslBundles getSslBundles() { return this.sslBundles; } - protected boolean isSslEnabled() { + protected final boolean isSslEnabled() { return getProperties().getSsl().isEnabled(); } + protected final boolean urlUsesSsl() { + return RedisUrl.of(this.properties.getUrl()).useSsl(); + } + protected boolean isPoolEnabled(Pool pool) { Boolean enabled = pool.getEnabled(); return (enabled != null) ? enabled : COMMONS_POOL2_AVAILABLE; @@ -173,89 +174,8 @@ abstract class RedisConnectionConfiguration { return nodes; } - protected final boolean urlUsesSsl() { - return ConnectionInfo.of(this.properties.getUrl()).isUseSsl(); - } - protected final RedisConnectionDetails getConnectionDetails() { return this.connectionDetails; } - static final class ConnectionInfo { - - private final URI uri; - - private final boolean useSsl; - - private final String username; - - private final String password; - - private final int database; - - private ConnectionInfo(URI uri, boolean useSsl, String username, String password, int database) { - this.uri = uri; - this.useSsl = useSsl; - this.username = username; - this.password = password; - this.database = database; - } - - URI getUri() { - return this.uri; - } - - boolean isUseSsl() { - return this.useSsl; - } - - String getUsername() { - return this.username; - } - - String getPassword() { - return this.password; - } - - int getDatabase() { - return this.database; - } - - static ConnectionInfo of(String url) { - try { - URI uri = new URI(url); - String scheme = uri.getScheme(); - if (!"redis".equals(scheme) && !"rediss".equals(scheme)) { - throw new RedisUrlSyntaxException(url); - } - boolean useSsl = ("rediss".equals(scheme)); - String username = null; - String password = null; - if (uri.getUserInfo() != null) { - String candidate = uri.getUserInfo(); - int index = candidate.indexOf(':'); - if (index >= 0) { - username = candidate.substring(0, index); - password = candidate.substring(index + 1); - } - else { - password = candidate; - } - } - int database = 0; - if (StringUtils.hasText(uri.getPath())) { - String[] pathSplit = uri.getPath().split("/", 2); - if (pathSplit.length > 1 && !pathSplit[1].isEmpty()) { - database = Integer.parseInt(pathSplit[1]); - } - } - return new ConnectionInfo(uri, useSsl, username, password, database); - } - catch (URISyntaxException ex) { - throw new RedisUrlSyntaxException(url, ex); - } - } - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrl.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrl.java new file mode 100644 index 00000000000..7639b16e77f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrl.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.redis; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.springframework.util.StringUtils; + +/** + * A parsed URL used to connect to Redis. + * + * @param uri the source URI + * @param useSsl if SSL is used to connect + * @param credentials the connection credentials + * @param database the database index + * @author Mark Paluch + * @author Stephane Nicoll + * @author Alen Turkovic + * @author Scott Frederick + * @author EddĂș MelĂ©ndez + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Yanming Zhou + * @author Phillip Webb + */ +record RedisUrl(URI uri, boolean useSsl, Credentials credentials, int database) { + + static RedisUrl of(String url) { + return (url != null) ? of(toUri(url)) : null; + } + + private static RedisUrl of(URI uri) { + boolean useSsl = ("rediss".equals(uri.getScheme())); + Credentials credentials = Credentials.fromUserInfo(uri.getUserInfo()); + int database = getDatabase(uri); + return new RedisUrl(uri, useSsl, credentials, database); + } + + private static int getDatabase(URI uri) { + String path = uri.getPath(); + String[] split = (!StringUtils.hasText(path)) ? new String[0] : path.split("/", 2); + return (split.length > 1 && !split[1].isEmpty()) ? Integer.parseInt(split[1]) : 0; + } + + private static URI toUri(String url) { + try { + URI uri = new URI(url); + String scheme = uri.getScheme(); + if (!"redis".equals(scheme) && !"rediss".equals(scheme)) { + throw new RedisUrlSyntaxException(url); + } + return uri; + } + catch (URISyntaxException ex) { + throw new RedisUrlSyntaxException(url, ex); + } + } + + /** + * Redis connection credentials. + * + * @param username the username or {@code null} + * @param password the password + */ + record Credentials(String username, String password) { + + private static final Credentials NONE = new Credentials(null, null); + + private static Credentials fromUserInfo(String userInfo) { + if (userInfo == null) { + return NONE; + } + int index = userInfo.indexOf(':'); + if (index != -1) { + return new Credentials(userInfo.substring(0, index), userInfo.substring(index + 1)); + } + return new Credentials(null, userInfo); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java index aa25c2a8826..ffb020e29de 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java @@ -22,13 +22,13 @@ import java.io.InputStream; import javax.sql.DataSource; import javax.xml.XMLConstants; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import javax.xml.transform.Source; import javax.xml.transform.sax.SAXSource; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; -import jakarta.xml.bind.Unmarshaller; import org.jooq.ConnectionProvider; import org.jooq.DSLContext; import org.jooq.ExecuteListenerProvider; @@ -40,6 +40,8 @@ import org.jooq.impl.DefaultDSLContext; import org.jooq.impl.DefaultExecuteListenerProvider; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -138,27 +140,35 @@ public class JooqAutoConfiguration { } } + /** + * Load {@link Settings} with + * XML External Entity Prevention. + */ private static final class JaxbSettingsLoader { private Settings load(InputStream inputStream) { try { - // See - // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#jaxb-unmarshaller - SAXParserFactory spf = SAXParserFactory.newInstance(); - spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - spf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - spf.setNamespaceAware(true); - spf.setXIncludeAware(false); - Source xmlSource = new SAXSource(spf.newSAXParser().getXMLReader(), new InputSource(inputStream)); - JAXBContext jc = JAXBContext.newInstance(Settings.class); - Unmarshaller um = jc.createUnmarshaller(); - return um.unmarshal(xmlSource, Settings.class).getValue(); + SAXParser parser = createParserFactory().newSAXParser(); + Source source = new SAXSource(parser.getXMLReader(), new InputSource(inputStream)); + JAXBContext context = JAXBContext.newInstance(Settings.class); + return context.createUnmarshaller().unmarshal(source, Settings.class).getValue(); } catch (ParserConfigurationException | JAXBException | SAXException ex) { throw new IllegalStateException("Failed to unmarshal settings", ex); } } + private SAXParserFactory createParserFactory() + throws ParserConfigurationException, SAXNotRecognizedException, SAXNotSupportedException { + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setNamespaceAware(true); + factory.setXIncludeAware(false); + return factory; + } + } }