Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for Spring 3.1+ #24

Merged
merged 3 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading