Skip to content

Commit

Permalink
Merge pull request #24 from valb3r/bugfix/LTH-23-Spring-boot-3.1
Browse files Browse the repository at this point in the history
Fixes for Spring 3.1+
  • Loading branch information
valb3r authored Aug 12, 2023
2 parents f93c4b3 + c72d0b3 commit 1103b15
Show file tree
Hide file tree
Showing 20 changed files with 278 additions and 16 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ repositories {
}

ext {
spring = '6.0.3'
springBoot = '3.0.5'
spring = '6.0.11'
springBoot = '3.1.2'
acme4j = '2.16'
tomcatEmbed = '10.1.7'
jettyEmbed = '11.0.14'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.web.embedded.jetty.ConfigurableJettyWebServerFactory;
import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -71,7 +72,6 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

/**
* This configuration class is responsible for maintaining KeyStore with LetsEncrypt certificates.
Expand Down Expand Up @@ -191,7 +191,7 @@ public void customize(Server server) {
continue;
}
var ctx = factory.getSslContextFactory();
if (!ctx.getKeyStorePath().contains(serverProperties.getSsl().getKeyStore())
if (!ctx.getKeyStorePassword().equals(serverProperties.getSsl().getKeyStorePassword())
|| !ctx.getCertAlias().equals(serverProperties.getSsl().getKeyAlias())) {
continue;
}
Expand Down Expand Up @@ -245,6 +245,23 @@ WebServerFactoryCustomizer<ConfigurableJettyWebServerFactory> servletContainer()
};
}

@Configuration
public static class CustomTomcatServletWebServerFactoryCustomizer
implements WebServerFactoryCustomizer<JettyServletWebServerFactory> {

private final JettyWellKnownLetsEncryptChallengeEndpointConfig challengeEndpointConfig;

public CustomTomcatServletWebServerFactoryCustomizer(JettyWellKnownLetsEncryptChallengeEndpointConfig challengeEndpointConfig) {
this.challengeEndpointConfig = challengeEndpointConfig;
}

// For Spring Boot 3.1+ forcefully creating empty keystore if does not exist due to SslConnectorCustomizer
@Override
public void customize(JettyServletWebServerFactory factory) {
challengeEndpointConfig.createBasicKeystoreIfMissing();
}
}

protected Instant getNow() {
return Instant.now();
}
Expand Down Expand Up @@ -337,7 +354,9 @@ private void executeCheckCertValidityAndRotateIfNeeded() {

try {
updateCertificateAndKeystore(ks);
endpoint.getSslContextFactory().reload(it -> {});
endpoint.getSslContextFactory().reload(it -> {
it.setKeyStore(ks);
});
} catch (Exception ex) {
logger.warn("Failed updating KeyStore", ex);
}
Expand Down
34 changes: 34 additions & 0 deletions jetty/tests-spring-3.0/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
plugins {
id 'java'
}

repositories {
mavenCentral()
}

dependencies {
testImplementation project(":jetty")
testImplementation project(":tests-common")

testImplementation ("org.springframework.boot:spring-boot-starter-web:3.0.0") {
exclude module: 'spring-boot-starter-tomcat'
}
testImplementation "org.springframework.boot:spring-boot-starter-jetty:3.0.0"
testImplementation "org.springframework.boot:spring-boot-starter-test:3.0.0"
// See https://github.com/spring-projects/spring-boot/issues/33044 for details
testImplementation('jakarta.servlet:jakarta.servlet-api') {
version {
strictly '5.0.0'
because "Jetty 11 does not support Servlet 6 yet"
}
}

testImplementation "org.awaitility:awaitility:$awaitability"
testImplementation "org.junit.jupiter:junit-jupiter-engine:$jupiter"
testImplementation "org.testcontainers:junit-jupiter:$testContainers"
testImplementation "org.assertj:assertj-core:$assertj"
}

test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.github.valb3r.letsencrypthelper.jetty;

import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;

@Configuration
public class DisableJettySniConfig implements JettyServerCustomizer {

@Override
public void customize(Server server) {
Arrays.stream(server.getConnectors())
.forEach(
connector -> connector.getConnectionFactory(HttpConnectionFactory.class)
.getHttpConfiguration()
.getCustomizer(SecureRequestCustomizer.class)
.setSniHostCheck(false)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.github.valb3r.letsencrypthelper.jetty;

import com.github.valb3r.letsencrypthelper.ExpiredKeyPebbleTest;
import org.springframework.context.annotation.Import;

@Import(DisableJettySniConfig.class)
class JettyExpiredKeyPebbleTest extends ExpiredKeyPebbleTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.github.valb3r.letsencrypthelper.jetty;

import com.github.valb3r.letsencrypthelper.NoKeystorePebbleTest;
import org.springframework.context.annotation.Import;

@Import(DisableJettySniConfig.class)
class JettyNoKeystorePebbleTest extends NoKeystorePebbleTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.github.valb3r.letsencrypthelper.jetty;

import com.github.valb3r.letsencrypthelper.NotExpiredKeystorePebbleTest;
import org.springframework.context.annotation.Import;

@Import(DisableJettySniConfig.class)
class JettyNotExpiredKeystorePebbleTest extends NotExpiredKeystorePebbleTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.valb3r.letsencrypthelper.jetty.dummyapp;

import com.github.valb3r.letsencrypthelper.jetty.JettyWellKnownLetsEncryptChallengeEndpointConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import({JettyWellKnownLetsEncryptChallengeEndpointConfig.class})
public class DummyApp {

public static void main(String[] args) {
SpringApplication.run(DummyApp.class);
}
}
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ rootProject.name = 'letsencrypt-helper'
include 'tomcat'
include 'jetty'
include 'tests-common'
include 'tomcat:tests-spring-3.0'
include 'jetty:tests-spring-3.0'

project(":tomcat").projectDir = file("tomcat")
project(":jetty").projectDir = file("jetty")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ public class TomcatWellKnownLetsEncryptChallengeEndpointConfig implements Tomcat
private final List<TargetProtocol> observedProtocols = new CopyOnWriteArrayList<>();
private final AtomicBoolean customized = new AtomicBoolean();

// Internal
private SSLHostConfig sslHostConfig;

/**
* Initialize LetsEncrypt certificate obtaining and renewal class.
* @param serverProperties - SSL properties (serverProperties.ssl) to be used
Expand Down Expand Up @@ -199,9 +202,11 @@ public void customize(Connector connector) {
return;
}

this.sslHostConfig = sslConfig;

createBasicKeystoreIfMissing();

TargetProtocol observe = createObservableProtocol(protocol, sslConfig);
TargetProtocol observe = createObservableProtocol(protocol);
if (observe == null) {
return;
}
Expand Down Expand Up @@ -244,20 +249,42 @@ WebServerFactoryCustomizer<TomcatServletWebServerFactory> servletContainer() {
};
}

@Configuration
public static class CustomTomcatServletWebServerFactoryCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

private final TomcatWellKnownLetsEncryptChallengeEndpointConfig challengeEndpointConfig;

public CustomTomcatServletWebServerFactoryCustomizer(TomcatWellKnownLetsEncryptChallengeEndpointConfig challengeEndpointConfig) {
this.challengeEndpointConfig = challengeEndpointConfig;
}

// For Spring Boot 3.1+ forcefully creating empty keystore if does not exist due to SslConnectorCustomizer
@Override
public void customize(TomcatServletWebServerFactory factory) {
challengeEndpointConfig.createBasicKeystoreIfMissing();
}
}

protected Instant getNow() {
return Instant.now();
}

protected boolean matchesCertFilePathAndPassword(SSLHostConfig config, String password) {
// Spring Boot 3+
// Spring Boot 3.1+ Hacketty here
return null != findMatchingCertificate(config, password);
}

protected SSLHostConfigCertificate findMatchingCertificate(SSLHostConfig config, String password) {
if (null != config.getCertificates()) {
return config.getCertificates().stream()
.filter(it -> null != it.getCertificateKeystoreFile())
.filter(it -> it.getCertificateKeystoreFile().contains(serverProperties.getSsl().getKeyStore()))
.anyMatch(it -> password.equals(it.getCertificateKeystorePassword()));
.filter(it -> null != it.getCertificateKeyAlias())
.filter(it -> it.getCertificateKeyAlias().equals(config.getCertificates().stream().findFirst().get().getCertificateKeyAlias()))
.filter(it -> password.equals(it.getCertificateKeystorePassword()))
.findFirst().orElse(null);
}

return false;
return null;
}

private void createBasicKeystoreIfMissing() {
Expand All @@ -276,16 +303,16 @@ private void createBasicKeystoreIfMissing() {
logger.info("Created basic (dummy cert, real account/domain keys) KeyStore: {}", keystoreFile.getAbsolutePath());
}

private TargetProtocol createObservableProtocol(AbstractHttp11Protocol<?> protocol, SSLHostConfig sslConfig) {
var observe = new TargetProtocol(sslConfig, protocol);
private TargetProtocol createObservableProtocol(AbstractHttp11Protocol<?> protocol) {
var observe = new TargetProtocol(sslHostConfig, protocol);
var ks = tryToReadKeystore();
var cert = tryToReadCertificate(observe, ks);
if (null == cert) {
logger.warn(
"For Protocol {}:{} unable to read certificate from {}",
protocol.getClass().getCanonicalName(),
protocol.getPort(),
sslConfig.getCertificates().stream().map(SSLHostConfigCertificate::getCertificateKeystoreFile).collect(Collectors.toList())
sslHostConfig.getCertificates().stream().map(SSLHostConfigCertificate::getCertificateKeystoreFile).collect(Collectors.toList())
);
return null;
}
Expand Down Expand Up @@ -345,7 +372,9 @@ private void executeCheckCertValidityAndRotateIfNeeded() {
}

try {
updateCertificateAndKeystore(ks);
updateKeystoreAndSave(ks);
var certificate = findMatchingCertificate(sslHostConfig, keyPassword());
certificate.setCertificateKeystore(ks); // Since Spring 3.1 KeyStore is used instead of Keystore file
protocol.getProtocol().reloadSslHostConfigs();
} catch (RuntimeException ex) {
logger.warn("Failed updating KeyStore", ex);
Expand Down Expand Up @@ -393,7 +422,7 @@ private Certificate selfSign(KeyPair keyPair, Instant notBefore, Instant notAfte

}

private void updateCertificateAndKeystore(KeyStore ks) {
private void updateKeystoreAndSave(KeyStore ks) {
Session session = new Session(letsEncryptServer);
URI tos;
try {
Expand Down
24 changes: 24 additions & 0 deletions tomcat/tests-spring-3.0/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
id 'java'
}

repositories {
mavenCentral()
}

dependencies {
testImplementation project(":tomcat")
testImplementation project(":tests-common")
testImplementation "org.springframework.boot:spring-boot-starter-web:3.0.0"
testImplementation "org.springframework.boot:spring-boot-starter-tomcat:3.0.0"
testImplementation "org.springframework.boot:spring-boot-starter-test:3.0.0"

testImplementation "org.awaitility:awaitility:$awaitability"
testImplementation "org.junit.jupiter:junit-jupiter-engine:$jupiter"
testImplementation "org.testcontainers:junit-jupiter:$testContainers"
testImplementation "org.assertj:assertj-core:$assertj"
}

test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.github.valb3r.letsencrypthelper.tomcat;

import com.github.valb3r.letsencrypthelper.ExpiredKeyPebbleTest;

class TomcatExpiredKeyPebbleTest extends ExpiredKeyPebbleTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.github.valb3r.letsencrypthelper.tomcat;

import com.github.valb3r.letsencrypthelper.NoKeystorePebbleTest;

class TomcatNoKeystorePebbleTest extends NoKeystorePebbleTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.github.valb3r.letsencrypthelper.tomcat;

import com.github.valb3r.letsencrypthelper.NotExpiredKeystorePebbleTest;

class TomcatNotExpiredKeystorePebbleTest extends NotExpiredKeystorePebbleTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.valb3r.letsencrypthelper.tomcat.dummyapp;

import com.github.valb3r.letsencrypthelper.tomcat.TomcatWellKnownLetsEncryptChallengeEndpointConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import({TomcatWellKnownLetsEncryptChallengeEndpointConfig.class})
public class DummyApp {

public static void main(String[] args) {
SpringApplication.run(DummyApp.class);
}
}
25 changes: 25 additions & 0 deletions tomcat/tests-spring-3.2-m1/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id 'java'
}

repositories {
mavenCentral()
maven { url "https://repo.spring.io/milestone" }
}

dependencies {
testImplementation project(":tomcat")
testImplementation project(":tests-common")
testImplementation "org.springframework.boot:spring-boot-starter-web:3.2.0-M1"
testImplementation "org.springframework.boot:spring-boot-starter-tomcat:3.2.0-M1"
testImplementation "org.springframework.boot:spring-boot-starter-test:3.2.0-M1"

testImplementation "org.awaitility:awaitility:$awaitability"
testImplementation "org.junit.jupiter:junit-jupiter-engine:$jupiter"
testImplementation "org.testcontainers:junit-jupiter:1.18.3"
testImplementation "org.assertj:assertj-core:$assertj"
}

test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.github.valb3r.letsencrypthelper.tomcat;

import com.github.valb3r.letsencrypthelper.ExpiredKeyPebbleTest;

class TomcatExpiredKeyPebbleTest extends ExpiredKeyPebbleTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.github.valb3r.letsencrypthelper.tomcat;

import com.github.valb3r.letsencrypthelper.NoKeystorePebbleTest;

class TomcatNoKeystorePebbleTest extends NoKeystorePebbleTest {
}
Loading

0 comments on commit 1103b15

Please sign in to comment.