├── examples └── hello-world-acme │ ├── src │ └── main │ │ ├── resources │ │ ├── application-tls.yml │ │ ├── application-http.yml │ │ ├── application-dns.yml │ │ ├── application.yml │ │ └── logback.xml │ │ └── java │ │ └── com │ │ └── acme │ │ └── example │ │ ├── Application.java │ │ └── HelloWorldController.java │ ├── micronaut-cli.yml │ ├── docs │ └── images │ │ ├── Acme_cert_micronaut.png │ │ ├── EC2_Security_Group.png │ │ ├── Route_53_Management_Console.png │ │ └── Instances__EC2_Management_Console.png │ ├── .gitignore │ ├── Dockerfile │ ├── build.gradle │ └── README.md ├── acme ├── src │ ├── main │ │ ├── resources │ │ │ ├── application-challenge.yml │ │ │ └── META-INF │ │ │ │ └── native-image │ │ │ │ └── io.micronaut.acme │ │ │ │ └── micronaut-acme │ │ │ │ └── resource-config.json │ │ └── java │ │ │ └── io │ │ │ └── micronaut │ │ │ └── acme │ │ │ ├── ssl │ │ │ ├── package-info.java │ │ │ ├── DelegatedSslContext.java │ │ │ └── AcmeSSLContextBuilder.java │ │ │ ├── events │ │ │ ├── package-info.java │ │ │ └── CertificateEvent.java │ │ │ ├── services │ │ │ ├── package-info.java │ │ │ └── AcmeRuntimeException.java │ │ │ ├── background │ │ │ ├── package-info.java │ │ │ └── AcmeCertRefresherTask.java │ │ │ ├── challenge │ │ │ ├── http │ │ │ │ ├── package-info.java │ │ │ │ └── endpoint │ │ │ │ │ ├── HttpChallengeDetails.java │ │ │ │ │ └── WellKnownTokenController.java │ │ │ └── dns │ │ │ │ ├── DnsChallengeSolver.java │ │ │ │ └── RenderedTextDnsChallengeSolver.java │ │ │ └── package-info.java │ └── test │ │ ├── groovy │ │ └── io │ │ │ └── micronaut │ │ │ ├── mock │ │ │ └── slow │ │ │ │ ├── SlowServerConfig.groovy │ │ │ │ └── SlowAcmeServer.groovy │ │ │ └── acme │ │ │ ├── challenge │ │ │ └── http │ │ │ │ └── endpoint │ │ │ │ └── WellKnownTokenControllerSpec.groovy │ │ │ ├── AcmeCertRefresherTaskSpec.groovy │ │ │ ├── challenges │ │ │ ├── AcmeCertRefresherTaskTlsApln01ChallengeSpec.groovy │ │ │ ├── AcmeCertRefresherTaskHttp01ChallengeSpec.groovy │ │ │ └── AcmeCertRefresherTaskDns01ChallengeSpec.groovy │ │ │ ├── AcmeCertRefresherMultiDomainsTaskSpec.groovy │ │ │ ├── AcmeCertRefresherTaskUnitSpec.groovy │ │ │ ├── AcmeCertRefresherTaskWithFileKeysSpec.groovy │ │ │ ├── AcmeCertWildcardRefresherTaskSpec.groovy │ │ │ ├── AcmeCertRefresherTaskWithClasspathKeysSpec.groovy │ │ │ ├── AcmeCertRefresherTaskSetsTimeoutSpec.groovy │ │ │ ├── events │ │ │ └── CertificateEventSpec.groovy │ │ │ └── AcmeBaseSpec.groovy │ │ └── resources │ │ ├── logback.xml │ │ ├── test-domain.pem │ │ └── test-account.pem └── build.gradle ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── license.gradle └── libs.versions.toml ├── src └── main │ └── docs │ └── guide │ ├── challenges.adoc │ ├── introduction.adoc │ ├── repository.adoc │ ├── releaseHistory.adoc │ ├── cli.adoc │ ├── challenges │ ├── tls.adoc │ ├── http.adoc │ └── dns.adoc │ ├── toc.yml │ ├── configuration.adoc │ └── cli │ └── usage.adoc ├── .github ├── ISSUE_TEMPLATE │ ├── other.yaml │ ├── new_feature.yaml │ ├── config.yml │ └── bug_report.yaml ├── renovate.json ├── release.yml ├── workflows │ ├── publish-snapshot.yml │ ├── central-sync.yml │ ├── graalvm-latest.yml │ ├── graalvm-dev.yml │ ├── gradle.yml │ └── release.yml └── instructions │ ├── docs.instructions.md │ └── coding.instructions.md ├── config ├── accepted-api-changes.json ├── checkstyle │ ├── suppressions.xml │ └── checkstyle.xml ├── HEADER └── spotless.license.java ├── acme-bom └── build.gradle ├── gradle.properties ├── .gitattributes ├── .editorconfig ├── .clineignore ├── SECURITY.md ├── settings.gradle ├── .gitignore ├── ISSUE_TEMPLATE.md ├── .clinerules ├── docs.md └── coding.md ├── gradlew.bat ├── README.md ├── CONTRIBUTING.md └── gradlew /examples/hello-world-acme/src/main/resources/application-tls.yml: -------------------------------------------------------------------------------- 1 | acme: 2 | challenge-type: 'tls' 3 | 4 | -------------------------------------------------------------------------------- /acme/src/main/resources/application-challenge.yml: -------------------------------------------------------------------------------- 1 | micronaut: 2 | server: 3 | port: 8086 4 | ssl: 5 | enabled: false 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micronaut-projects/micronaut-acme/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/hello-world-acme/micronaut-cli.yml: -------------------------------------------------------------------------------- 1 | profile: service 2 | defaultPackage: acme.example.app 3 | --- 4 | testFramework: junit 5 | sourceLanguage: java -------------------------------------------------------------------------------- /src/main/docs/guide/challenges.adoc: -------------------------------------------------------------------------------- 1 | ACME supports 3 different challenge types which will be used to validate that you in fact own the domain before a certificate is issued. -------------------------------------------------------------------------------- /src/main/docs/guide/introduction.adoc: -------------------------------------------------------------------------------- 1 | Micronaut supports https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment[ACME] via the `micronaut-acme` module. 2 | -------------------------------------------------------------------------------- /src/main/docs/guide/repository.adoc: -------------------------------------------------------------------------------- 1 | You can find the source code of this project in this repository: 2 | 3 | https://github.com/{githubSlug}[https://github.com/{githubSlug}] 4 | -------------------------------------------------------------------------------- /examples/hello-world-acme/docs/images/Acme_cert_micronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micronaut-projects/micronaut-acme/HEAD/examples/hello-world-acme/docs/images/Acme_cert_micronaut.png -------------------------------------------------------------------------------- /examples/hello-world-acme/docs/images/EC2_Security_Group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micronaut-projects/micronaut-acme/HEAD/examples/hello-world-acme/docs/images/EC2_Security_Group.png -------------------------------------------------------------------------------- /examples/hello-world-acme/.gitignore: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | .DS_Store 3 | .gradle 4 | build/ 5 | target/ 6 | out/ 7 | .idea 8 | *.iml 9 | *.ipr 10 | *.iws 11 | .project 12 | .settings 13 | .classpath -------------------------------------------------------------------------------- /examples/hello-world-acme/docs/images/Route_53_Management_Console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micronaut-projects/micronaut-acme/HEAD/examples/hello-world-acme/docs/images/Route_53_Management_Console.png -------------------------------------------------------------------------------- /acme/src/main/resources/META-INF/native-image/io.micronaut.acme/micronaut-acme/resource-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": [ 3 | { "pattern": "\\Qorg/shredzone/acme4j/version.properties\\E" } 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/main/docs/guide/releaseHistory.adoc: -------------------------------------------------------------------------------- 1 | For this project, you can find a list of releases (with release notes) here: 2 |  3 | https://github.com/{githubSlug}/releases[https://github.com/{githubSlug}/releases] 4 | -------------------------------------------------------------------------------- /examples/hello-world-acme/docs/images/Instances__EC2_Management_Console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micronaut-projects/micronaut-acme/HEAD/examples/hello-world-acme/docs/images/Instances__EC2_Management_Console.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.yaml: -------------------------------------------------------------------------------- 1 | name: Other 2 | description: Something different 3 | body: 4 | - type: textarea 5 | attributes: 6 | label: Issue description 7 | validations: 8 | required: true 9 | 10 | -------------------------------------------------------------------------------- /examples/hello-world-acme/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM adoptopenjdk/openjdk11-openj9:jdk-11.0.1.13-alpine-slim 2 | COPY build example.jar 3 | EXPOSE 8080 4 | CMD java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar example.jar -------------------------------------------------------------------------------- /src/main/docs/guide/cli.adoc: -------------------------------------------------------------------------------- 1 | To be able to secure your application using ACME there will be a few setup steps necessary before you can start using 2 | the new certificates. Support for ACME and this setup has been baked into micronaut-starter (https://github.com/micronaut-projects/micronaut-starter). 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/mock/slow/SlowServerConfig.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.mock.slow 2 | 3 | import java.time.Duration 4 | 5 | interface SlowServerConfig { 6 | boolean isSlowSignup() 7 | boolean isSlowAuthorization() 8 | boolean isSlowOrdering() 9 | Duration getDuration() 10 | } 11 | -------------------------------------------------------------------------------- /config/accepted-api-changes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "io.micronaut.acme.events.CertificateEvent", 4 | "member": "Constructor io.micronaut.acme.events.CertificateEvent(java.security.cert.X509Certificate,java.security.KeyPair,boolean)", 5 | "reason": "Removed deprecated constructor for Micronaut 4" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /acme-bom/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "io.micronaut.build.internal.bom" 3 | } 4 | 5 | micronautBom { 6 | suppressions { 7 | // as of micronaut-acme 5.0.0 8 | // acme removed this acme4j-utils and included it in the acme4j-client lib as of v3.0.0 9 | acceptedLibraryRegressions.add("acme4j-utils") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | projectVersion=6.0.0-SNAPSHOT 2 | projectGroup=io.micronaut.acme 3 | 4 | title=Micronaut Acme 5 | projectDesc=Extensions to integrate Micronaut and Acme 6 | projectUrl=https://micronaut.io 7 | githubSlug=micronaut-projects/micronaut-acme 8 | developers=Nathan Zender 9 | 10 | 11 | org.gradle.caching=true 12 | org.gradle.parallel=true 13 | -------------------------------------------------------------------------------- /src/main/docs/guide/challenges/tls.adoc: -------------------------------------------------------------------------------- 1 | Utilizing `tls` challenge type is the simplest to configure and thus the default because you will only be required 2 | to open up the default secure port for the server to allow the challenge server to validate it. 3 | 4 | .src/main/resources/application.yml 5 | [source,yaml] 6 | ---- 7 | acme: 8 | challenge-type: 'tls' 9 | ---- -------------------------------------------------------------------------------- /src/main/docs/guide/toc.yml: -------------------------------------------------------------------------------- 1 | introduction: Introduction 2 | releaseHistory: Release History 3 | configuration: 4 | title: Configuration 5 | challenges: 6 | title: Challenge Types 7 | http: 8 | title: HTTP-01 9 | tls: 10 | title: TLS-APLN-01 11 | dns: 12 | title: DNS-01 13 | cli: 14 | title: CLI 15 | usage: 16 | title: Usage 17 | repository: Repository 18 | -------------------------------------------------------------------------------- /examples/hello-world-acme/src/main/resources/application-http.yml: -------------------------------------------------------------------------------- 1 | acme: 2 | challenge-type: 'http' 3 | 4 | micronaut: 5 | server: 6 | # Let's encrypt only talks to 80 and since we are not using a load balancer or anything 7 | # fancy we will just do this but it will require running with `sudo` which is not idea 8 | # in a production environment. 9 | port : 80 10 | dual-protocol: true -------------------------------------------------------------------------------- /examples/hello-world-acme/src/main/resources/application-dns.yml: -------------------------------------------------------------------------------- 1 | acme: 2 | challenge-type: 'dns' 3 | auth: 4 | # Due to the current manual nature in which the dns validation is performed by default 5 | # we change the amount of time we wait before trying to authorize again to make sure there 6 | # is time for us logging into the dns interface, setting a TXT record and waiting for it 7 | # to propagate. 8 | pause: 2m 9 | 10 | 11 | -------------------------------------------------------------------------------- /config/checkstyle/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new_feature.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Create a new feature request 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Please describe the feature you want for Micronaut to implement, before that check if there is already an existing issue to add it. 8 | - type: textarea 9 | attributes: 10 | label: Feature description 11 | placeholder: Tell us what feature you would like for Micronaut to have and what problem is it going to solve 12 | validations: 13 | required: true 14 | 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.java text eol=lf 5 | *.groovy text eol=lf 6 | *.html text eol=lf 7 | *.kt text eol=lf 8 | *.kts text eol=lf 9 | *.md text diff=markdown eol=lf 10 | *.py text diff=python executable 11 | *.pl text diff=perl executable 12 | *.pm text diff=perl 13 | *.css text diff=css eol=lf 14 | *.js text eol=lf 15 | *.sql text eol=lf 16 | *.q text eol=lf 17 | 18 | *.sh text eol=lf 19 | gradlew text eol=lf 20 | 21 | *.bat text eol=crlf 22 | *.cmd text eol=crlf 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | 9 | [{*.sh,gradlew}] 10 | end_of_line = lf 11 | 12 | [{*.bat,*.cmd}] 13 | end_of_line = crlf 14 | 15 | [{*.mustache,*.ftl}] 16 | insert_final_newline = false 17 | 18 | [*.java] 19 | indent_size = 4 20 | tab_width = 4 21 | max_line_length = 100 22 | # Import order can be configured with ij_java_imports_layout=... 23 | # See documentation https://youtrack.jetbrains.com/issue/IDEA-170643#focus=streamItem-27-3708697.0-0 24 | 25 | [*.xml] 26 | indent_size = 4 27 | -------------------------------------------------------------------------------- /config/HEADER: -------------------------------------------------------------------------------- 1 | Copyright ${year} original authors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Micronaut Core Discussions 3 | url: https://github.com/micronaut-projects/micronaut-core/discussions 4 | about: Ask questions about Micronaut on Github 5 | - name: Micronaut Data Discussions 6 | url: https://github.com/micronaut-projects/micronaut-data/discussions 7 | about: Ask Micronaut Data related questions on Github 8 | - name: Stack Overflow 9 | url: https://stackoverflow.com/tags/micronaut 10 | about: Ask questions on Stack Overflow 11 | - name: Chat 12 | url: https://gitter.im/micronautfw/ 13 | about: Chat with us on Gitter. -------------------------------------------------------------------------------- /src/main/docs/guide/challenges/http.adoc: -------------------------------------------------------------------------------- 1 | Utilizing `http` challenge type you will need to do one of the following two things : 2 | 3 | 1. enable dual protocol support 4 | 2. setup redirect from http -> https in any load balancer/proxy server you have in front of your application. 5 | 6 | 7 | The reason for this is that Let's Encrypt for example will only call out to 8 | the http challenge type over http and nothing else but will follow redirects. 9 | 10 | .src/main/resources/application.yml 11 | [source,yaml] 12 | ---- 13 | acme: 14 | challenge-type: 'http' 15 | 16 | micronaut: 17 | server: 18 | dual-protocol: true 19 | ---- -------------------------------------------------------------------------------- /gradle/license.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.github.hierynomus.license' 2 | 3 | license { 4 | header = rootProject.file('config/HEADER') 5 | strictCheck = true 6 | ignoreFailures = true 7 | mapping { 8 | kt = 'SLASHSTAR_STYLE' 9 | java = 'SLASHSTAR_STYLE' 10 | groovy = 'SLASHSTAR_STYLE' 11 | } 12 | ext.year = '2017-2020' 13 | 14 | exclude "**/transaction/**" 15 | exclude '**/*.txt' 16 | exclude '**/*.html' 17 | exclude '**/*.xml' 18 | exclude '**/*.json' 19 | exclude '**/build-info.properties' 20 | exclude '**/git.properties' 21 | exclude '**/othergit.properties' 22 | } 23 | -------------------------------------------------------------------------------- /.clineignore: -------------------------------------------------------------------------------- 1 | # .clineignore - Cline AI ignore file for Micronaut projects 2 | # This file tells Cline which files/directories to ignore for code intelligence and automation 3 | 4 | # === Build outputs === 5 | build/ 6 | */build/ 7 | !build/docs/ 8 | !build/docs/** 9 | !build/generated/ 10 | !build/generated/** 11 | 12 | # === Dependency/Cache directories === 13 | .gradle/ 14 | */.gradle/ 15 | 16 | # === IDE/Editor/OS Metadata === 17 | .vscode/ 18 | .idea/ 19 | .DS_Store 20 | *.swp 21 | *.swo 22 | *.bak 23 | *.tmp 24 | *.orig 25 | 26 | # === Tool-specific/Config artifacts === 27 | *.log 28 | 29 | # === Gradle Wrappers/Binaries === 30 | gradlew 31 | gradlew.bat 32 | -------------------------------------------------------------------------------- /config/spotless.license.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-$YEAR original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ -------------------------------------------------------------------------------- /examples/hello-world-acme/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | micronaut: 2 | application: 3 | name: example 4 | server: 5 | ssl: 6 | enabled: true 7 | ssl: 8 | # Let's encrypt only talks to 443 and since we are not using a load balancer or anything 9 | # fancy we will just do this but it will require running with `sudo` which is not idea 10 | # in a production environment. 11 | port: 443 12 | 13 | acme: 14 | enabled: true 15 | tos-agree: true 16 | acme-server: https://acme-v02.api.letsencrypt.org/directory 17 | # If you want to use the staging server, it would look something like so. 18 | # acme-server: https://acme-staging-v02.api.letsencrypt.org/directory 19 | cert-location: /tmp 20 | -------------------------------------------------------------------------------- /examples/hello-world-acme/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 7 | 8 | %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:recommended" 4 | ], 5 | "addLabels": [ 6 | "type: dependency-upgrade" 7 | ], 8 | "schedule": [ 9 | "after 10pm" 10 | ], 11 | "prHourlyLimit": 1, 12 | "prConcurrentLimit": 20, 13 | "timezone": "Europe/Prague", 14 | "packageRules": [ 15 | { 16 | "dependencyDashboardApproval": true, 17 | "matchUpdateTypes": [ 18 | "patch" 19 | ], 20 | "matchCurrentVersion": "!/^0/", 21 | "automerge": true, 22 | "matchPackageNames": [ 23 | "/actions.*/" 24 | ] 25 | }, 26 | { 27 | "matchUpdateTypes": [ 28 | "patch" 29 | ], 30 | "matchCurrentVersion": "!/^0/", 31 | "automerge": true 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We release patches for security vulnerabilities. Which versions are eligible 4 | receiving such patches depend on the CVSS v3.0 Rating: 5 | 6 | | CVSS v3.0 | Supported Versions | 7 | |-----------|-------------------------------------------| 8 | | 9.0-10.0 | Releases within the previous three months | 9 | | 4.0-8.9 | Most recent release | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please responsibly disclose (suspected) security vulnerabilities to 14 | **[The Micronaut Foundation](foundation@micronaut.io)**. You will receive a response from 15 | us within 48 hours. If the issue is confirmed, we will release a patch as soon 16 | as possible depending on complexity but historically within a few days. 17 | -------------------------------------------------------------------------------- /acme/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "io.micronaut.build.internal.acme-module" 3 | } 4 | 5 | dependencies { 6 | annotationProcessor(mnValidation.micronaut.validation.processor) 7 | implementation(mnValidation.micronaut.validation) 8 | implementation mn.micronaut.http 9 | implementation mn.micronaut.http.server 10 | implementation mn.micronaut.http.server.netty 11 | implementation libs.managed.acme4j.client 12 | implementation libs.netty.tcnative.boringssl.static 13 | compileOnly(mn.micronaut.http.netty.http3) 14 | testImplementation(mnSerde.micronaut.serde.jackson) 15 | testImplementation(mnTestResources.testcontainers.core) 16 | testImplementation libs.groovy.json 17 | testImplementation libs.groovy.dateutil 18 | testImplementation mn.micronaut.http.client 19 | } 20 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/ssl/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Netty SSL. 18 | * 19 | * @author Nathan Zender 20 | * @since 1.0 21 | */ 22 | package io.micronaut.acme.ssl; 23 | -------------------------------------------------------------------------------- /acme/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 7 | 8 | %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/events/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Events used by ACME tasks. 18 | * 19 | * @author Nathan Zender 20 | * @since 1.0 21 | */ 22 | package io.micronaut.acme.events; 23 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | plugins { 9 | id 'io.micronaut.build.shared.settings' version '8.0.0-M12' 10 | } 11 | 12 | rootProject.name = "acme-parent" 13 | 14 | include 'acme-bom' 15 | include "acme" 16 | 17 | [ 18 | "hello-world-acme" 19 | ].each { name -> 20 | include name 21 | def project = findProject(":${name}") 22 | project.name = "micronaut-acme-example-${name}" 23 | project.projectDir = new File(settingsDir, "examples/${name}") 24 | } 25 | 26 | micronautBuild { 27 | useStandardizedProjectNames=true 28 | importMicronautCatalog() 29 | importMicronautCatalog("micronaut-serde") 30 | importMicronautCatalog("micronaut-test-resources") 31 | importMicronautCatalog("micronaut-validation") 32 | } 33 | 34 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/services/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Service classes used to integrate ACME. 18 | * 19 | * @author Nathan Zender 20 | * @since 1.0 21 | */ 22 | package io.micronaut.acme.services; 23 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - micronaut-build 5 | categories: 6 | - title: Breaking Changes 🛠 7 | labels: 8 | - 'type: breaking' 9 | - title: New Features 🎉 10 | labels: 11 | - 'type: enhancement' 12 | - title: Bug Fixes 🐞 13 | labels: 14 | - 'type: bug' 15 | - title: Improvements ⭐ 16 | labels: 17 | - 'type: improvement' 18 | - title: Docs 📖 19 | labels: 20 | - 'type: docs' 21 | - title: Dependency updates 🚀 22 | labels: 23 | - 'type: dependency-upgrade' 24 | - 'dependency-upgrade' 25 | - title: Regressions 🧐 26 | labels: 27 | - 'type: regression' 28 | - title: GraalVM 🏆 29 | labels: 30 | - 'relates-to: graal' 31 | - title: Other Changes 💡 32 | labels: 33 | - "*" 34 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/background/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Background jobs used to integrate ACME. 18 | * 19 | * @author Nathan Zender 20 | * @since 1.0 21 | */ 22 | package io.micronaut.acme.background; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .DS_Store 3 | target/ 4 | .gradle/ 5 | .idea/ 6 | build/ 7 | !build-logic/src/main/java/io/micronaut/build 8 | !build-logic/src/main/kotlin/io/micronaut/build 9 | classes/ 10 | out/ 11 | *.db 12 | *.log 13 | *.iml 14 | .classpath 15 | .factorypath 16 | bin/ 17 | .settings/ 18 | .project 19 | */test/ 20 | */META-INF/ 21 | *.ipr 22 | *.iws 23 | .kotlintest 24 | */.kotlintest/ 25 | 26 | # ignore resources, are downloaded via a gradle task from micronaut_docs 27 | src/main/docs/resources/css/highlight/*.css 28 | src/main/docs/resources/css/highlight/*.png 29 | src/main/docs/resources/css/highlight/*.jpg 30 | src/main/docs/resources/css/*.css 31 | src/main/docs/resources/js/*.js 32 | src/main/docs/resources/style/*.html 33 | src/main/docs/resources/img/micronaut-logo-white.svg 34 | 35 | # Ignore files generated by test-resources 36 | **/.micronaut/test-resources/ 37 | 38 | # Ignore gradle.properties generated by micronaut-build 39 | /buildSrc/gradle.properties 40 | -------------------------------------------------------------------------------- /examples/hello-world-acme/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("io.micronaut.application") 3 | } 4 | 5 | version = "0.1" 6 | group = "io.micronaut.example" 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | micronaut { 13 | version libs.versions.micronaut.platform.get() 14 | runtime("netty") 15 | testRuntime("junit5") 16 | processing { 17 | incremental(true) 18 | annotations("com.example.*") 19 | } 20 | } 21 | 22 | dependencies { 23 | annotationProcessor("io.micronaut.validation:micronaut-validation-processor") 24 | implementation("io.micronaut.validation:micronaut-validation") 25 | implementation(project(":micronaut-acme")) 26 | implementation("io.micronaut:micronaut-http-client") 27 | implementation("io.micronaut:micronaut-runtime") 28 | implementation("javax.annotation:javax.annotation-api") 29 | runtimeOnly("ch.qos.logback:logback-classic") 30 | runtimeOnly("org.yaml:snakeyaml") 31 | } 32 | 33 | application { 34 | mainClass.set("com.acme.example.Application") 35 | } 36 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/challenge/http/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Http Challenge code used by ACME challenge server. 18 | * 19 | * @author Nathan Zender 20 | * @since 1.0 21 | */ 22 | @Configuration 23 | @Requires(property = "acme.challenge-type", value = "http") 24 | package io.micronaut.acme.challenge.http; 25 | 26 | import io.micronaut.context.annotation.Configuration; 27 | import io.micronaut.context.annotation.Requires; 28 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/services/AcmeRuntimeException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micronaut.acme.services; 17 | 18 | /** 19 | * Acme scoped runtime exception to be used for anything Acme related. 20 | */ 21 | public class AcmeRuntimeException extends RuntimeException { 22 | 23 | /** 24 | * @param message Message detailing more details about Acme exception 25 | */ 26 | public AcmeRuntimeException(String message) { 27 | super(message); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/hello-world-acme/src/main/java/com/acme/example/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.acme.example; 17 | 18 | import io.micronaut.runtime.Micronaut; 19 | 20 | /** 21 | * Simple example application to show off how Acme integration works. 22 | */ 23 | public class Application { 24 | 25 | /** 26 | * Main program. 27 | * @param args whatever you want to pass 28 | */ 29 | public static void main(String[] args) { 30 | Micronaut.run(Application.class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thanks for reporting an issue, please review the task list below before submitting the 2 | issue. Your issue report will be closed if the issue is incomplete and the below tasks not completed. 3 | 4 | NOTE: If you are unsure about something and the issue is more of a question a better place to ask questions is on Stack Overflow (https://stackoverflow.com/tags/micronaut) or Gitter (https://gitter.im/micronautfw/). DO NOT use the issue tracker to ask questions. 5 | 6 | ### Task List 7 | 8 | - [ ] Steps to reproduce provided 9 | - [ ] Stacktrace (if present) provided 10 | - [ ] Example that reproduces the problem uploaded to Github 11 | - [ ] Full description of the issue provided (see below) 12 | 13 | ### Steps to Reproduce 14 | 15 | 1. TODO 16 | 2. TODO 17 | 3. TODO 18 | 19 | ### Expected Behaviour 20 | 21 | Tell us what should happen 22 | 23 | ### Actual Behaviour 24 | 25 | Tell us what happens instead 26 | 27 | ### Environment Information 28 | 29 | - **Operating System**: TODO 30 | - **Micronaut Version:** TODO 31 | - **JDK Version:** TODO 32 | 33 | ### Example Application 34 | 35 | - TODO: link to github repository with example that reproduces the issue 36 | 37 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | micronaut = "4.10.9" 3 | micronaut-platform = "4.10.1" 4 | managed-acme4j = "3.5.1" 5 | micronaut-serde = "2.16.2" 6 | micronaut-test-resources = "2.10.1" 7 | micronaut-validation = "4.12.0" 8 | micronaut-gradle-plugin = "4.6.1" 9 | [libraries] 10 | # Core 11 | micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut' } 12 | 13 | managed-acme4j-client = { module = 'org.shredzone.acme4j:acme4j-client', version.ref = 'managed-acme4j' } 14 | 15 | micronaut-serde = { module = 'io.micronaut.serde:micronaut-serde-bom', version.ref = 'micronaut-serde' } 16 | micronaut-test-resources = { module = "io.micronaut.testresources:micronaut-test-resources-bom", version.ref = "micronaut-test-resources" } 17 | micronaut-validation = { module = "io.micronaut.validation:micronaut-validation-bom", version.ref = "micronaut-validation" } 18 | 19 | graal = { module = "org.graalvm.nativeimage:svm" } 20 | netty-tcnative-boringssl-static = { module = 'io.netty:netty-tcnative-boringssl-static' } 21 | 22 | groovy-json = { module = 'org.apache.groovy:groovy-json' } 23 | groovy-dateutil = { module = 'org.apache.groovy:groovy-dateutil' } 24 | gradle-micronaut = { module = "io.micronaut.gradle:micronaut-gradle-plugin", version.ref = "micronaut-gradle-plugin" } 25 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Micronaut Acme integration. 18 | * 19 | * @author Nathan Zender 20 | * @since 1.0 21 | */ 22 | @Configuration 23 | @Requires(property = "acme.enabled", value = TRUE) 24 | @Requires(property = ServerSslConfiguration.PREFIX + ".enabled", value = TRUE, defaultValue = FALSE) 25 | package io.micronaut.acme; 26 | 27 | import io.micronaut.context.annotation.Configuration; 28 | import io.micronaut.context.annotation.Requires; 29 | import io.micronaut.http.ssl.ServerSslConfiguration; 30 | 31 | import static io.micronaut.core.util.StringUtils.FALSE; 32 | import static io.micronaut.core.util.StringUtils.TRUE; 33 | -------------------------------------------------------------------------------- /examples/hello-world-acme/src/main/java/com/acme/example/HelloWorldController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.acme.example; 17 | 18 | import io.micronaut.http.MediaType; 19 | import io.micronaut.http.annotation.Controller; 20 | import io.micronaut.http.annotation.Get; 21 | import io.micronaut.http.annotation.Produces; 22 | 23 | /** 24 | * Simple hello world endpoint. 25 | */ 26 | @Controller("/helloWorld") 27 | public class HelloWorldController { 28 | 29 | /** 30 | * Gets a simple text message back that allows validation server is up and secure. 31 | * @return String message 32 | */ 33 | @Get 34 | @Produces(MediaType.TEXT_PLAIN) 35 | public String index() { 36 | return "Hello Secured World"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Do not edit this file directly. Instead, go to: 2 | # 3 | # https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows 4 | # 5 | # and edit them there. Note that it will be sync'ed to all the Micronaut repos 6 | name: Publish snapshot release 7 | on: [workflow_dispatch] 8 | jobs: 9 | build: 10 | if: github.repository != 'micronaut-projects/micronaut-project-template' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Remove system JDKs 14 | run: | 15 | sudo rm -rf /usr/lib/jvm/* 16 | unset JAVA_HOME 17 | export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '/usr/lib/jvm' | paste -sd:) 18 | - uses: actions/checkout@v6 19 | - uses: actions/cache@v4 20 | with: 21 | path: ~/.gradle/caches 22 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 23 | restore-keys: | 24 | ${{ runner.os }}-gradle- 25 | - name: Set up JDK 26 | uses: actions/setup-java@v5 27 | with: 28 | distribution: 'temurin' 29 | java-version: | 30 | 21 31 | 25 32 | - name: Publish to Sonatype Snapshots 33 | if: success() 34 | env: 35 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 36 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 37 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 38 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 39 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 40 | run: ./gradlew publishToSonatype --no-daemon 41 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/challenge/http/endpoint/HttpChallengeDetails.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micronaut.acme.challenge.http.endpoint; 17 | 18 | /** 19 | * Contains the details needed to satisfy a passing http-01 challenge from the acme challenge server. 20 | */ 21 | public final class HttpChallengeDetails { 22 | private final String token; 23 | private final String content; 24 | 25 | /** 26 | * Constructs a new http challenge token/content pair. 27 | * @param token passed from challenge server 28 | * @param content expected content back to the challenge server 29 | */ 30 | public HttpChallengeDetails(String token, String content) { 31 | this.token = token; 32 | this.content = content; 33 | } 34 | 35 | /** 36 | * @return the content expected to be returned to the challenge server. 37 | */ 38 | public String getContent() { 39 | return content; 40 | } 41 | 42 | /** 43 | * @return token expected to be passed from the challenge server. 44 | */ 45 | public String getToken() { 46 | return token; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /acme/src/test/resources/test-domain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAlu9BtvxJ63B9FDM/l2z8hKm5y5PNL6PcjuL17tyh9ElbPMRf 3 | WImthc2Nl6zBd1Pao4HVsnhSC7aCJpx1kruhEgvntUf1EdifAjsgEu8mRzmrnFD9 4 | Ju5ni7bRAFEyJ4QBMpfjvDzFLV9Tz36QJePxZLu9LJqJBHQxEnVRi+m4L7NCtjYV 5 | fMF3IM34OVRqZdbQYsIKZbrgTDMDxqEQSRHZjh5M0zJwwc3H545UAPiAt3MWIQoS 6 | eOelGhzjfyNho+0owyORPsmqD5DWLaSvUiHDRtHfU5UyfPvUudddqS+WjXyNms4+ 7 | Uu7n/ygeoYouCRH7+82flJ4w/aWOBIpiwlsNHQIDAQABAoIBAE0mUOXSwYoJ/rVD 8 | mN6zA8Rf6StpCjmuvX9//Yux1UrD8FH4YnAkN8EsF5MO5/kxsJFhPTUzkWSRTqej 9 | 0+lD5QoPccnU1SzhGC4QwM1M1rkTfmexciTjOaRNtzk9R7CxJdeRkgPW4EbX8kQe 10 | gloL0IjX+dOBzkWriqXPt/YXamFeq+A3mYJF8dY5bycTNQO5V8g4mx4aUmWG+Dyi 11 | mtzugOfuFcXbGdd38sZI84PBsHr1eGtGZijigQ5iyyt/o5/49pZNeAxvo83AlxcO 12 | un2LFBTxYohvPPz8vM8tcExbGCNglE7mowDH06K8lQqDativ+MQU5oS6CqlTvNkI 13 | 1ils2/UCgYEAx/X2Q8RcGOZvoCAGFCPzF7m9kAbBEF3fSzPXXPDXB2007/PXwkq0 14 | 5ACoJazgvNffNSgXITXXus0zS0t5R3vot7RWYzX71zfDeyuV8zNRTArLoHiGvOkE 15 | kcWtrKGr7PhdCs1X9JLtVMrvtzXkzqExagALy55Ko8/6AIxCHFJUkOsCgYEAwTvy 16 | cC+pV86ys5XQjmdg6LV8fksoHnoO/Y+YQL2m32Bi4Xwhlx1QEFK+03t+V5awPjre 17 | 1TyLzOrvb7o6AhJXEBgGZJdr9zlADFIjwPx7Zc2IUwgxJpx9Is/kVijgWUTYiMZk 18 | xALxo2KuoLJapXGu7o7108wEEFnQpUi8cKKEGBcCgYANjyQv3DTSi22uUf2XiAiH 19 | 51RvW8XjsjneA5nq93ndSw535vUOe7pga5r7Uwm6RHkiRaGr4tbKF/gOdwO2UTSq 20 | oFPrTlHRejqLM51rbGNq4KCGNSYN0U86A0mPlzbtTrogbWQ1dXEaetheeA0X5d5P 21 | 7cje+dDd4tB1EQ26leqeGQKBgQCseJQwm+nbTMtlzR9EK3Ns4agHlY9ufGVq3kL3 22 | 7g7Gq+I1/jSBC5HNr/1RB5XCGeae9K61xv/E3CDwKVjjRnldDQSPvjOIixnmpV3y 23 | P6joOaYm7lXob56ldscIPB3ar79RfTqtVS9WNJtHJUoRB0Iq/YDfFOa/rVq0XBKN 24 | uhCK6wKBgFk7iYpMrm6/wsXBJdXe6z/1f9mjG1tIuwjr0mYiCnP9u6DjB4cZ08f3 25 | lyUBGm9NlyCVYlRXdYiNGnt832Vs5h835k1NunfYMHWuAaQv8cM3L6d4y2kM2VY8 26 | IjsbPZAS6WE085A9aJnEMy9VXSE/4ldzuKj1InWFjCj3UzDiPAnl 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /acme/src/test/resources/test-account.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAt0c9z3mCsScA5jUC9/4IOP4I4ZZ9ct9RXZMKDGaotmKBwXdz 3 | hTuJ7ivBjYO+dmEqJRnazp1/HktAJojzPnKl0mNYFpsJ/jeoTGeVVh+4oqG89iJk 4 | XMFXXpWEyZ2/U+nPJgXeF/nC6OuqbVHeY6fJM5dpfuXIXD7GUw63eAF21LqzyH2e 5 | 2dDqdZjwh6igV+acpQf0TWUzoP7gg8TdTPM/Vqao/AbUjkZOmG9sHCi8Cj4zYJlG 6 | UvK/xdk7oI0Czs5CccWEOqdDk0eECX3IBRYAfE3pWItN4Rjh8f8TlGVZaAfVa8k0 7 | f13boX1nxSmaFKmHyB774wS7KN764PxK2C+4sQIDAQABAoIBAQCpUKdJhHe4MNAj 8 | 29ViRMxT2ltaDCzYcnY4fB5MVoqF83rrv+54pwhFfybQFWVe92R34erB9b35vosF 9 | Dn+osUF/m0dFmvUgZUqVSxwq9CMeG0z2Fc+h4PtV5ctGdvTKELXN6p3CiHoHs68+ 10 | TuOkLN0zTC2pjZ4LmfKdyHq14qRk4IX+QfWJP1WxGs4vVFxOwV04GSbctozJObPY 11 | 4bscogj1BfVUBTPl6kSHD2YDi5T3gE45ZnotCj8V6HKa4XS4Ge/pV1b1wV2/sxMd 12 | f+IKQ9Ox9KW2xv2r4fZsVG3309QvQVPsjvu64MTjaQmv0AwhihiLAMKh+sxt7I8f 13 | X9t1G+IBAoGBANjwGinKqeIqNmgdg1j/nl9YuVyBW9QPv6jD2u2NPyr+Sp8m+gqh 14 | 9+LIL5dyxXgLAdntbkhKvOKY+l9S+o4wzRCk7wb0PT00ZD6he2vm2RTSr6gOTsL/ 15 | CgmDwPT4S17c7z1iM0Zoqo0e3HXcqq2f0MxGam6Rgxg63/yVjCm88rTPAoGBANhH 16 | k62R59lGCcZXmQ1JAJyU3z10gRRWfxhHq5xoLBKfvQJqc5aYSelwo38RXTVe8V2J 17 | IqflAxKe5vFlkmO7Kz2xear0C3+Zz2/SjOpq7pYxKPq/ORLJTGZxLGskNlefB81s 18 | wMmDoxIlV/WXeUIH91Y1SQ+bZQadHMX06e1UpRp/AoGAFCs2c345DyLXjhR4WrTh 19 | N4IbMaOBMxUHv5v95aoFHm0n6OYJxyVJ05bC/fSYsVFsqaMuZqA0MWkBlg0z6DZX 20 | Sl3bLy1T6DXPwBbpT53Vvt7bn+c8oVpux3WtYdkXwMkPoQhZNgmTGa2t13NdlujN 21 | 08AUMxVqN715h5Urw9GiSvcCgYEAt/HwhQ/yC5YI3DtGfckYDxSC5aa/3cdPIxxZ 22 | tZXX3iMjwvk8w9lUC4n0VC81gh301KO86OTa/yxMqQTFQ7M9rKPUIfScDvOHPMjr 23 | drhpoS3Ad7rJVNQF+Z7Js3pCCbXFEg8rzHf76oP+Um94/xL9ZsG9GNwGSWC4xxht 24 | GKAEKAsCgYBi6pXzXZ4R+ep8siYfSwZnAqJ957pi7Dqa5aYFE9MT/H8Z0maieUlJ 25 | GYMb0q27Wa9Tzjp0oFlbr+ty6dub2yZrLjbldayXm5DBEEzhKYC2FM5FwClYx8Zk 26 | b1TQV/nYIcxUsHhthDrSl+7LJ1H3u4tk6uDDsbajjU6TqdmjIHTWIg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /.github/workflows/central-sync.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Do not edit this file directly. Instead, go to: 2 | # 3 | # https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows 4 | # 5 | # and edit them there. Note that it will be sync'ed to all the Micronaut repos 6 | name: Maven Central Sync 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | release_version: 11 | description: 'Release version (eg: 1.2.3)' 12 | required: true 13 | jobs: 14 | central-sync: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Remove system JDKs 18 | run: | 19 | sudo rm -rf /usr/lib/jvm/* 20 | unset JAVA_HOME 21 | export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '/usr/lib/jvm' | paste -sd:) 22 | - name: Checkout repository 23 | uses: actions/checkout@v6 24 | with: 25 | ref: v${{ github.event.inputs.release_version }} 26 | - uses: gradle/actions/wrapper-validation@v5 27 | - name: Set up JDK 28 | uses: actions/setup-java@v5 29 | with: 30 | distribution: 'temurin' 31 | java-version: | 32 | 21 33 | 25 34 | - name: Publish to Sonatype OSSRH 35 | env: 36 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 37 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 38 | GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} 39 | GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }} 40 | GPG_FILE: ${{ secrets.GPG_FILE }} 41 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 42 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 43 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 44 | run: | 45 | echo $GPG_FILE | base64 -d > secring.gpg 46 | ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository 47 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/challenge/dns/DnsChallengeSolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2021 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micronaut.acme.challenge.dns; 17 | 18 | import io.micronaut.context.annotation.DefaultImplementation; 19 | 20 | /** 21 | * Represents a solver for the DNS challenge that can create and destroy 22 | * DNS records. 23 | */ 24 | @DefaultImplementation(RenderedTextDnsChallengeSolver.class) 25 | public interface DnsChallengeSolver { 26 | /** 27 | * Creates the TXT record for `_acme-challenge.domain` 28 | * with a value of digest to verify the domain. 29 | * 30 | *

This method should block and only return once the TXT record has been 31 | * created, however {@see io.micronaut.acme.AcmeConfiguration} `pause` setting can also be 32 | * used to provide time for propagation.

33 | * 34 | * @param domain The domain to create the record for, excluding the `_acme-challenge` key 35 | * @param digest The value to set the TXT record to for the challenge to succeed 36 | */ 37 | void createRecord(String domain, String digest); 38 | 39 | /** 40 | * Remove the TXT record previously created for the challenge. 41 | * 42 | *

This method is called even if the challenge failed, so it is possible the record may not exist.

43 | * 44 | * @param domain The domain to remove the record for, excluding the `_acme-challenge` key 45 | */ 46 | void destroyRecord(String domain); 47 | } 48 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/challenge/dns/RenderedTextDnsChallengeSolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2021 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micronaut.acme.challenge.dns; 17 | 18 | import jakarta.inject.Singleton; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | /** 23 | * Default DNS challenge solver which simply prints instructions to STDOUT to manually create a record. 24 | */ 25 | @Singleton 26 | class RenderedTextDnsChallengeSolver implements DnsChallengeSolver { 27 | private static final String HEADER = 28 | "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"; 29 | 30 | private static final String TXT_RECORD_NAME = "_acme-challenge"; 31 | private static final Logger LOG = LoggerFactory.getLogger(RenderedTextDnsChallengeSolver.class); 32 | 33 | @Override 34 | public void createRecord(String domain, String digest) { 35 | LOG.info(HEADER); 36 | LOG.info(HEADER); 37 | LOG.info("\t\t\t\t\t\t\tCREATE DNS `TXT` ENTRY AS FOLLOWS"); 38 | LOG.info("\t\t\t\t{}.{} with value {}", TXT_RECORD_NAME, domain, digest); 39 | LOG.info(HEADER); 40 | LOG.info(HEADER); 41 | } 42 | 43 | @Override 44 | public void destroyRecord(String domain) { 45 | // To maintain backwards compatibility with <=v3.0.1, do not print text 46 | 47 | if (LOG.isDebugEnabled()) { 48 | LOG.debug("The 'TXT' record for " + TXT_RECORD_NAME + "." + domain + " can be removed"); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/challenge/http/endpoint/WellKnownTokenControllerSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme.challenge.http.endpoint 2 | 3 | import groovy.json.JsonSlurper 4 | import io.micronaut.acme.AcmeBaseSpec 5 | import io.micronaut.http.HttpRequest 6 | import io.micronaut.http.HttpResponse 7 | import io.micronaut.http.HttpStatus 8 | import io.micronaut.http.client.exceptions.HttpClientResponseException 9 | 10 | import static org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric 11 | 12 | class WellKnownTokenControllerSpec extends AcmeBaseSpec { 13 | 14 | Map getConfiguration(){ 15 | super.getConfiguration() << [ 16 | "acme.domains": EXPECTED_ACME_DOMAIN, 17 | "acme.challenge-type" : "http", 18 | "micronaut.server.dualProtocol": true, 19 | "micronaut.server.port" : expectedHttpPort 20 | ] 21 | } 22 | 23 | void "pass invalid token"(){ 24 | given: 25 | def details = new HttpChallengeDetails(randomAlphanumeric(10), randomAlphanumeric(10)) 26 | embeddedServer.applicationContext.publishEvent(details) 27 | 28 | when: 29 | callWellKnownEndpoint(randomAlphanumeric(10)) 30 | 31 | then: 32 | def ex = thrown(HttpClientResponseException) 33 | ex.response.status() == HttpStatus.NOT_FOUND 34 | new JsonSlurper().parseText(ex.response.body()).message == "Not Found" 35 | } 36 | 37 | void "pass valid token"(){ 38 | given : 39 | def token = randomAlphanumeric(10) 40 | def details = new HttpChallengeDetails(token, randomAlphanumeric(10)) 41 | 42 | and: 43 | embeddedServer.applicationContext.publishEvent(details) 44 | 45 | when: 46 | HttpResponse response = callWellKnownEndpoint(token) 47 | 48 | then: 49 | response.status() == HttpStatus.OK 50 | response.body() == details.content 51 | } 52 | 53 | private HttpResponse callWellKnownEndpoint(String randomToken) { 54 | client.toBlocking().exchange(HttpRequest.GET("/.well-known/acme-challenge/$randomToken"), String) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for reporting an issue, please review the task list below before submitting the issue. Your issue report will be closed if the issue is incomplete and the below tasks not completed. 8 | 9 | NOTE: If you are unsure about something and the issue is more of a question a better place to ask questions is on Github Discussions :arrow_up:, [Stack Overflow](https://stackoverflow.com/tags/micronaut) or [Gitter](https://gitter.im/micronautfw/). 10 | - type: textarea 11 | attributes: 12 | label: Expected Behavior 13 | description: A concise description of what you expected to happen. 14 | placeholder: Tell us what should happen 15 | validations: 16 | required: false 17 | - type: textarea 18 | attributes: 19 | label: Actual Behaviour 20 | description: A concise description of what you're experiencing. 21 | placeholder: Tell us what happens instead 22 | validations: 23 | required: false 24 | - type: textarea 25 | attributes: 26 | label: Steps To Reproduce 27 | description: Steps to reproduce the behavior. 28 | placeholder: | 29 | 1. In this environment... 30 | 2. With this config... 31 | 3. Run '...' 32 | 4. See error... 33 | validations: 34 | required: false 35 | - type: textarea 36 | attributes: 37 | label: Environment Information 38 | description: Environment information where the problem occurs. 39 | placeholder: | 40 | - Operating System: 41 | - JDK Version: 42 | validations: 43 | required: false 44 | - type: input 45 | id: example 46 | attributes: 47 | label: Example Application 48 | description: Example application link. 49 | placeholder: | 50 | Link to GitHub repository with an example that reproduces the issue 51 | validations: 52 | required: false 53 | - type: input 54 | id: version 55 | attributes: 56 | label: Version 57 | description: Micronaut version 58 | validations: 59 | required: true 60 | 61 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/challenge/http/endpoint/WellKnownTokenController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micronaut.acme.challenge.http.endpoint; 17 | 18 | import io.micronaut.http.HttpStatus; 19 | import io.micronaut.http.annotation.Controller; 20 | import io.micronaut.http.annotation.Get; 21 | import io.micronaut.http.annotation.PathVariable; 22 | import io.micronaut.http.exceptions.HttpStatusException; 23 | import io.micronaut.runtime.event.annotation.EventListener; 24 | 25 | /** 26 | * Endpoint to enable http-01 validation from the acme challenge server. 27 | */ 28 | @Controller("/.well-known/acme-challenge") 29 | public final class WellKnownTokenController { 30 | private HttpChallengeDetails challengeDetails = new HttpChallengeDetails("notreal", "notreal"); 31 | 32 | /** 33 | * Does validation to make sure token is as expected and then returns the correct content the challenge server needs. 34 | * @param token passed from the challenge server 35 | * @return content that the challenge server is expecting 36 | */ 37 | @Get("/{token}") 38 | String validateToken(@PathVariable String token) { 39 | if (challengeDetails.getToken().equalsIgnoreCase(token)) { 40 | return challengeDetails.getContent(); 41 | } else { 42 | throw new HttpStatusException(HttpStatus.NOT_FOUND, "Not found"); 43 | } 44 | } 45 | 46 | /** 47 | * Event listener to allow for passing in a new set of http challenge details. 48 | * @param challengeDetails details allowing for challenge verification 49 | */ 50 | @EventListener 51 | public void challengeDetails(HttpChallengeDetails challengeDetails) { 52 | this.challengeDetails = challengeDetails; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/docs/guide/challenges/dns.adoc: -------------------------------------------------------------------------------- 1 | Utilizing `dns` challenge type allows validation to be done via entry of a DNS TXT record. 2 | 3 | === Manual Verification 4 | By default, the application will log out a message that looks as follows. 5 | 6 | .DNS output 7 | [source] 8 | ---- 9 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 10 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 11 | CREATE DNS `TXT` ENTRY AS FOLLOWS 12 | _acme-challenge.example.com with value 79ZNJaxlcLYIFootHL6Rrbh2VUCfFGgPeurVyjoRrS8 13 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 14 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 15 | ---- 16 | 17 | Once this is output you will need to log into your DNS provider and create a TXT entry with the following key and value. 18 | 19 | * key: `_acme-challenge.example.com` 20 | * value: `79ZNJaxlcLYIFootHL6Rrbh2VUCfFGgPeurVyjoRrS8` 21 | 22 | Since this is a manual process you will also want to bump up your `acme.auth.pause` duration so that there is enough time between retries 23 | and time take for the manual entry/DNS propagation. 24 | 25 | .src/main/resources/application.yml 26 | [source,yaml] 27 | ---- 28 | acme: 29 | challenge-type: 'dns' 30 | auth: 31 | # Due to the current manual nature in which the dns validation is performed by default 32 | # we change the amount of time we wait before trying to authorize again to make sure there 33 | # is time for us logging into the dns interface, setting a TXT record and waiting for it 34 | # to propagate. 35 | pause: 2m 36 | ---- 37 | 38 | === Automatic Verification 39 | 40 | An implementation of api:acme.challenge.dns.DnsChallengeSolver[] can be provided to automate the creation and cleanup of the necessary DNS records. 41 | 42 | .CustomDnsChallengeSolver.java 43 | [source, java] 44 | ---- 45 | @Singleton 46 | @Replaces(DnsChallengeSolver.class) 47 | class CustomDnsChallengeSolver implements DnsChallengeSolver { 48 | @Override 49 | public void createRecord(String domain, String digest) { 50 | // Create a TXT record for $domain with the key "_acme-challenge" and the value of $digest 51 | } 52 | 53 | @Override 54 | public void destroyRecord(String domain) { 55 | // Remove the TXT record for $domain with the key "_acme-challenge" if it exists 56 | } 57 | } 58 | ---- 59 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/events/CertificateEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micronaut.acme.events; 17 | 18 | import org.jspecify.annotations.NonNull; 19 | import java.security.KeyPair; 20 | import java.security.cert.X509Certificate; 21 | 22 | /** 23 | * Event used to alert when a new ACME certificate is ready for use. 24 | */ 25 | public class CertificateEvent { 26 | private final KeyPair domainKeyPair; 27 | private final X509Certificate[] fullCertificateChain; 28 | private boolean validationCert; 29 | 30 | /** 31 | * Creates a new CertificateEvent containing the full certificate chain. 32 | * @param domainKeyPair key pair used to encrypt the certificate 33 | * @param validationCert if this certificate is to be used for tls-apln-01 account validation 34 | * @param fullCertificateChain X509 certificate file 35 | */ 36 | public CertificateEvent(KeyPair domainKeyPair, boolean validationCert, X509Certificate... fullCertificateChain) { 37 | if (fullCertificateChain == null || fullCertificateChain.length == 0) { 38 | throw new IllegalArgumentException("Certificate chain must not be empty"); 39 | } 40 | this.validationCert = validationCert; 41 | this.domainKeyPair = domainKeyPair; 42 | this.fullCertificateChain = fullCertificateChain; 43 | } 44 | 45 | /** 46 | * @return Certificate created by ACME server 47 | */ 48 | public X509Certificate getCert() { 49 | return fullCertificateChain[0]; 50 | } 51 | 52 | /** 53 | * @return KeyPair used to encrypt the certificate. 54 | */ 55 | public KeyPair getDomainKeyPair() { 56 | return domainKeyPair; 57 | } 58 | 59 | /** 60 | * @return if this is a validation certificate to be used for tls-apln-01 account validation 61 | */ 62 | public boolean isValidationCert() { 63 | return validationCert; 64 | } 65 | 66 | /** 67 | * Return the full certificate chain. 68 | * 69 | * @return array of certificates in the chain. 70 | */ 71 | @NonNull 72 | public X509Certificate[] getFullCertificateChain() { 73 | return fullCertificateChain; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/ssl/DelegatedSslContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micronaut.acme.ssl; 17 | 18 | import io.netty.buffer.ByteBufAllocator; 19 | import io.netty.handler.ssl.ApplicationProtocolNegotiator; 20 | import io.netty.handler.ssl.SslContext; 21 | 22 | import javax.net.ssl.SSLEngine; 23 | import javax.net.ssl.SSLSessionContext; 24 | import java.util.List; 25 | 26 | /** 27 | * Allows for netty SslContext to be delegated to another as well as switched out at runtime. 28 | */ 29 | public class DelegatedSslContext extends SslContext { 30 | 31 | private SslContext ctx; 32 | 33 | /** 34 | * Creates a new DelegatedSslContext with the SslContext to be delegated to. 35 | * 36 | * @param ctx {@link SslContext} 37 | */ 38 | DelegatedSslContext(SslContext ctx) { 39 | this.ctx = ctx; 40 | } 41 | 42 | /** 43 | * Overrides the existing delegated SslContext with the one passed. 44 | * 45 | * @param sslContext {@link SslContext} 46 | */ 47 | final void setNewSslContext(SslContext sslContext) { 48 | this.ctx = sslContext; 49 | } 50 | 51 | @Override 52 | public final boolean isClient() { 53 | return ctx.isClient(); 54 | } 55 | 56 | @Override 57 | public final List cipherSuites() { 58 | return ctx.cipherSuites(); 59 | } 60 | 61 | @Override 62 | public final long sessionCacheSize() { 63 | return ctx.sessionCacheSize(); 64 | } 65 | 66 | @Override 67 | public final long sessionTimeout() { 68 | return ctx.sessionTimeout(); 69 | } 70 | 71 | @Override 72 | public final ApplicationProtocolNegotiator applicationProtocolNegotiator() { 73 | return ctx.applicationProtocolNegotiator(); 74 | } 75 | 76 | @Override 77 | public final SSLEngine newEngine(ByteBufAllocator alloc) { 78 | return ctx.newEngine(alloc); 79 | } 80 | 81 | @Override 82 | public final SSLEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort) { 83 | return ctx.newEngine(alloc, peerHost, peerPort); 84 | } 85 | 86 | @Override 87 | public final SSLSessionContext sessionContext() { 88 | return ctx.sessionContext(); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /.github/workflows/graalvm-latest.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Do not edit this file directly. Instead, go to: 2 | # 3 | # https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows 4 | # 5 | # and edit them there. Note that it will be sync'ed to all the Micronaut repos 6 | name: GraalVM Latest CI 7 | on: 8 | push: 9 | branches: 10 | - master 11 | - '[0-9]+.[0-9]+.x' 12 | pull_request: 13 | branches: 14 | - master 15 | - '[0-9]+.[0-9]+.x' 16 | jobs: 17 | build_matrix: 18 | if: github.repository != 'micronaut-projects/micronaut-project-template' 19 | runs-on: ubuntu-latest 20 | env: 21 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 22 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 23 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 24 | outputs: 25 | matrix: ${{ steps.build-matrix.outputs.matrix }} 26 | steps: 27 | - uses: actions/checkout@v6 28 | - name: Build Matrix 29 | uses: micronaut-projects/github-actions/graalvm/build-matrix@master 30 | id: build-matrix 31 | with: 32 | java-version: '21' 33 | build: 34 | needs: build_matrix 35 | if: github.repository != 'micronaut-projects/micronaut-project-template' 36 | runs-on: ubuntu-latest 37 | strategy: 38 | max-parallel: 6 39 | matrix: 40 | java: ['21'] 41 | native_test_task: ${{ fromJson(needs.build_matrix.outputs.matrix).native_test_task }} 42 | env: 43 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 44 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 45 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 46 | steps: 47 | - name: Remove system JDKs 48 | run: | 49 | sudo rm -rf /usr/lib/jvm/* 50 | unset JAVA_HOME 51 | export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '/usr/lib/jvm' | paste -sd:) 52 | - uses: actions/checkout@v6 53 | - name: Pre-Build Steps 54 | uses: micronaut-projects/github-actions/graalvm/pre-build@master 55 | id: pre-build 56 | with: 57 | distribution: 'graalvm' 58 | gradle-java: '21' 59 | java: ${{ matrix.java }} 60 | nativeTestTask: ${{ matrix.native_test_task }} 61 | - name: Build Steps 62 | uses: micronaut-projects/github-actions/graalvm/build@master 63 | id: build 64 | env: 65 | GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} 66 | GH_USERNAME: ${{ secrets.GH_USERNAME }} 67 | GRAALVM_QUICK_BUILD: true 68 | with: 69 | nativeTestTask: ${{ matrix.native_test_task }} 70 | - name: Post-Build Steps 71 | uses: micronaut-projects/github-actions/graalvm/post-build@master 72 | id: post-build 73 | with: 74 | java: ${{ matrix.java }} 75 | -------------------------------------------------------------------------------- /.clinerules/docs.md: -------------------------------------------------------------------------------- 1 | ## Brief overview 2 | - Documentation for the Micronaut modules is primarily written in Asciidoc format, focusing on user guides, API references, and integration examples. The documentation emphasizes clarity, completeness, and practical examples to help developers integrate Micronaut modules effectively. Insights from the codebase show a focus on modular documentation aligned with subprojects, including setup instructions, usage examples, and troubleshooting tips. 3 | - All the files are within `src/main/docs/guide`. In that directory, there is a `toc.yml` file that is used to generate the table of contents and decide which `.adoc` files are to be included. 4 | 5 | ## Development workflow 6 | - Write documentation in Asciidoc: Place source files in the appropriate `src/main/docs` directory. 7 | - Build and assemble the documentation guide: Use `./gradlew docs` from the root directory to generate HTML documentation. Since the output of this task may be huge, ignore the output and check the last process exit code to tell if it works. Otherwise, ask the user. If it works, verify the output for formatting and content accuracy. 8 | - Once assembled, the guide will be at `build/docs/`. 9 | - Include examples: Create and reference code examples from the doc-examples/ directory, ensuring they are testable and up-to-date with the latest service versions. 10 | - Test documentation: Run builds regularly and check for broken links or outdated information. Integrate doc checks into CI pipelines using Gradle tasks. 11 | - Review and update: Conduct peer reviews for new documentation or changes, ensuring alignment with coding standards and project updates. 12 | 13 | ## Documentation best practices 14 | - Follow Asciidoc conventions: Use consistent headings, lists, code blocks, and admonitions (e.g., NOTE, TIP, WARNING) for better readability. 15 | - Provide comprehensive coverage: Include installation instructions, configuration details, usage examples, error handling, and performance tips for each service. 16 | - Use practical examples: Incorporate runnable code snippets from doc-examples/ to demonstrate real-world usage, with clear explanations and expected outputs. 17 | - Ensure accessibility: Use descriptive alt text for images, maintain logical structure, and avoid jargon without explanations. 18 | - Version control: Document version-specific changes and maintain backward compatibility notes. 19 | - Security and best practices: Highlight secure usage patterns, such as proper authentication and data handling. 20 | 21 | ## Project context 22 | - Focus on Micronaut-specific integration that this project is providing, emphasizing GraalVM compatibility, annotation-driven configurations, and modular design. 23 | - Prioritize user-centric content: Guides should facilitate quick starts, advanced customizations, and troubleshooting for developers building Micronaut applications. 24 | - Align with coding guidelines: Documentation should complement code by explaining architectural decisions, such as the use of factories, interceptors, and annotation processors. 25 | -------------------------------------------------------------------------------- /.github/instructions/docs.instructions.md: -------------------------------------------------------------------------------- 1 | ## Brief overview 2 | - Documentation for the Micronaut modules is primarily written in Asciidoc format, focusing on user guides, API references, and integration examples. The documentation emphasizes clarity, completeness, and practical examples to help developers integrate Micronaut modules effectively. Insights from the codebase show a focus on modular documentation aligned with subprojects, including setup instructions, usage examples, and troubleshooting tips. 3 | - All the files are within `src/main/docs/guide`. In that directory, there is a `toc.yml` file that is used to generate the table of contents and decide which `.adoc` files are to be included. 4 | 5 | ## Development workflow 6 | - Write documentation in Asciidoc: Place source files in the appropriate `src/main/docs` directory. 7 | - Build and assemble the documentation guide: Use `./gradlew docs` from the root directory to generate HTML documentation. Since the output of this task may be huge, ignore the output and check the last process exit code to tell if it works. Otherwise, ask the user. If it works, verify the output for formatting and content accuracy. 8 | - Once assembled, the guide will be at `build/docs/`. 9 | - Include examples: Create and reference code examples from the doc-examples/ directory, ensuring they are testable and up-to-date with the latest service versions. 10 | - Test documentation: Run builds regularly and check for broken links or outdated information. Integrate doc checks into CI pipelines using Gradle tasks. 11 | - Review and update: Conduct peer reviews for new documentation or changes, ensuring alignment with coding standards and project updates. 12 | 13 | ## Documentation best practices 14 | - Follow Asciidoc conventions: Use consistent headings, lists, code blocks, and admonitions (e.g., NOTE, TIP, WARNING) for better readability. 15 | - Provide comprehensive coverage: Include installation instructions, configuration details, usage examples, error handling, and performance tips for each service. 16 | - Use practical examples: Incorporate runnable code snippets from doc-examples/ to demonstrate real-world usage, with clear explanations and expected outputs. 17 | - Ensure accessibility: Use descriptive alt text for images, maintain logical structure, and avoid jargon without explanations. 18 | - Version control: Document version-specific changes and maintain backward compatibility notes. 19 | - Security and best practices: Highlight secure usage patterns, such as proper authentication and data handling. 20 | 21 | ## Project context 22 | - Focus on Micronaut-specific integration that this project is providing, emphasizing GraalVM compatibility, annotation-driven configurations, and modular design. 23 | - Prioritize user-centric content: Guides should facilitate quick starts, advanced customizations, and troubleshooting for developers building Micronaut applications. 24 | - Align with coding guidelines: Documentation should complement code by explaining architectural decisions, such as the use of factories, interceptors, and annotation processors. 25 | -------------------------------------------------------------------------------- /.github/workflows/graalvm-dev.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Do not edit this file directly. Instead, go to: 2 | # 3 | # https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows 4 | # 5 | # and edit them there. Note that it will be sync'ed to all the Micronaut repos 6 | name: GraalVM Dev CI 7 | on: 8 | schedule: 9 | - cron: "0 1 * * 1-5" # Mon-Fri at 1am UTC 10 | jobs: 11 | build_matrix: 12 | if: github.repository != 'micronaut-projects/micronaut-project-template' 13 | runs-on: ubuntu-latest 14 | env: 15 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 16 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 17 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 18 | outputs: 19 | matrix: ${{ steps.build-matrix.outputs.matrix }} 20 | steps: 21 | - uses: actions/checkout@v6 22 | - name: Build Matrix 23 | uses: micronaut-projects/github-actions/graalvm/build-matrix@master 24 | id: build-matrix 25 | build: 26 | needs: build_matrix 27 | if: github.repository != 'micronaut-projects/micronaut-project-template' 28 | runs-on: ubuntu-latest 29 | strategy: 30 | max-parallel: 6 31 | matrix: 32 | java: ['dev', 'latest-ea'] 33 | distribution: ['graalvm-community', 'graalvm'] 34 | native_test_task: ${{ fromJson(needs.build_matrix.outputs.matrix).native_test_task }} 35 | exclude: 36 | - java: 'dev' 37 | distribution: 'graalvm' 38 | - java: 'latest-ea' 39 | distribution: 'graalvm-community' 40 | env: 41 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 42 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 43 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 44 | steps: 45 | - name: Remove system JDKs 46 | run: | 47 | sudo rm -rf /usr/lib/jvm/* 48 | unset JAVA_HOME 49 | export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '/usr/lib/jvm' | paste -sd:) 50 | - uses: actions/checkout@v6 51 | - name: Pre-Build Steps 52 | uses: micronaut-projects/github-actions/graalvm/pre-build@master 53 | id: pre-build 54 | with: 55 | java: ${{ matrix.java }} 56 | distribution: ${{ matrix.distribution }} 57 | nativeTestTask: ${{ matrix.native_test_task }} 58 | - name: Build Steps 59 | uses: micronaut-projects/github-actions/graalvm/build@master 60 | id: build 61 | env: 62 | GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} 63 | GH_USERNAME: ${{ secrets.GH_USERNAME }} 64 | GRAALVM_QUICK_BUILD: true 65 | with: 66 | nativeTestTask: ${{ matrix.native_test_task }} 67 | - name: Post-Build Steps 68 | uses: micronaut-projects/github-actions/graalvm/post-build@master 69 | id: post-build 70 | with: 71 | java: ${{ matrix.java }} 72 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Micronaut Acme 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/io.micronaut.acme/micronaut-acme.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.micronaut.acme%22%20AND%20a:%22micronaut-acme%22) 4 | [![Build Status](https://github.com/micronaut-projects/micronaut-acme/workflows/Java%20CI/badge.svg)](https://github.com/micronaut-projects/micronaut-acme/actions) 5 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=micronaut-projects_micronaut-acme&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=micronaut-projects_micronaut-acme) 6 | [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.micronaut.io/scans) 7 | 8 | This project includes integration between [Micronaut](http://micronaut.io) and [ACME ](https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment) via [Acme4j](https://shredzone.org/maven/acme4j/index.html). 9 | 10 | The Micronaut ACME integration can be used together with any ACME server to provide ssl certificates for your application. [Let's Encrypt](https://letsencrypt.org/) is currently 11 | the front runner for integration with Acme and is completely free. 12 | 13 | ## Documentation ## 14 | 15 | See the [stable](https://micronaut-projects.github.io/micronaut-acme/latest/guide) or [snapshot](https://micronaut-projects.github.io/micronaut-acme/snapshot/guide) documentation for more information. 16 | 17 | ## ACME Tooling ## 18 | Since ACME servers do require some pre setup support has been baked into the micronaut-cli found [here](https://github.com/micronaut-projects/micronaut-starter). Which can help you create keys, create/deactivate accounts, etc. 19 | 20 | ## Example Application ## 21 | 22 | See the [Examples](https://github.com/micronaut-projects/micronaut-acme/tree/master/examples/hello-world-acme) for more information. 23 | 24 | ## Snapshots and Releases 25 | 26 | Snapshots are automatically published to [JFrog OSS](https://oss.jfrog.org/artifactory/oss-snapshot-local/) using [Github Actions](https://github.com/micronaut-projects/micronaut-acme/actions). 27 | 28 | See the documentation in the [Micronaut Docs](https://docs.micronaut.io/latest/guide/index.html#usingsnapshots) for how to configure your build to use snapshots. 29 | 30 | Releases are published to JCenter and Maven Central via [Github Actions](https://github.com/micronaut-projects/micronaut-acme/actions). 31 | 32 | A release is performed with the following steps: 33 | 34 | * [Create a new release](https://github.com/micronaut-projects/micronaut-acme/releases/new). The Git Tag should start with `v`. For example `v1.0.0`. 35 | * [Monitor the Workflow](https://github.com/micronaut-projects/micronaut-acme/actions?query=workflow%3ARelease) to check it passed successfully. 36 | * Celebrate! 37 | 38 | ## Building the micronaut-acme project 39 | 40 | #### Requirements 41 | 42 | * JDK 8 or later 43 | * To do a full build you will need a Docker Engine or Docker Desktop running as the tests require [TestContainers](https://www.testcontainers.org) 44 | 45 | #### Build Instructions 46 | 1. Checkout from Github (e.g. `git clone git@github.com:micronaut/micronaut-acme.git`) 47 | 2. `cd micronaut-acme` 48 | 3. `./gradlew build` 49 | 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Code or Documentation to Micronaut 2 | 3 | Sign the [Contributor License Agreement (CLA)](https://cla-assistant.io/micronaut-projects/micronaut-acme). This is required before any of your code or pull-requests are accepted. 4 | 5 | ## Finding Issues to Work on 6 | 7 | If you are interested in contributing to Micronaut and are looking for issues to work on, take a look at the issues tagged with [help wanted](https://github.com/micronaut-projects/micronaut-acme/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+help+wanted%22). 8 | 9 | ## JDK Setup 10 | 11 | Micronaut ACME currently requires JDK 8. 12 | 13 | ## IDE Setup 14 | 15 | Micronaut ACME can be imported into IntelliJ IDEA by opening the `build.gradle` file. 16 | 17 | ## Docker Setup 18 | 19 | Micronaut ACME tests currently require Docker to be installed. 20 | 21 | ## Running Tests 22 | 23 | To run the tests, use `./gradlew check`. 24 | 25 | ## Building Documentation 26 | 27 | The documentation sources are located at `src/main/docs/guide`. 28 | 29 | To build the documentation, run `./gradlew publishGuide` (or `./gradlew pG`), then open `build/docs/index.html` 30 | 31 | To also build the Javadocs, run `./gradlew docs`. 32 | 33 | ## Working on the code base 34 | 35 | If you use IntelliJ IDEA, you can import the project using the Intellij Gradle Tooling ("File / Import Project" and selecting the "settings.gradle" file). 36 | 37 | To get a local development version of Micronaut ACME working, first run the `publishToMavenLocal` task. 38 | 39 | ``` 40 | ./gradlew pTML 41 | ``` 42 | 43 | You can then reference the version specified with `projectVersion` in `gradle.properties` in a test project's `build.gradle` or `pom.xml`. If you use Gradle, add the `mavenLocal` repository (Maven automatically does this): 44 | 45 | ``` 46 | repositories { 47 | mavenLocal() 48 | mavenCentral() 49 | } 50 | ``` 51 | 52 | ## Creating a pull request 53 | 54 | Once you are satisfied with your changes: 55 | 56 | - Commit your changes in your local branch 57 | - Push your changes to your remote branch on GitHub 58 | - Send us a [pull request](https://help.github.com/articles/creating-a-pull-request) 59 | 60 | ## Checkstyle 61 | 62 | We want to keep the code clean, following good practices about organization, Javadoc, and style as much as possible. 63 | 64 | Micronaut ACME uses [Checkstyle](https://checkstyle.sourceforge.io/) to make sure that the code follows those standards. The configuration is defined in `config/checkstyle/checkstyle.xml`. To execute Checkstyle, run: 65 | 66 | ``` 67 | ./gradlew :checkstyleMain 68 | ``` 69 | 70 | Before starting to contribute new code we recommended that you install the IntelliJ [CheckStyle-IDEA](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea) plugin and configure it to use Micronaut's checkstyle configuration file. 71 | 72 | IntelliJ will mark in red the issues Checkstyle finds. For example: 73 | 74 | ![](https://github.com/micronaut-projects/micronaut-core/raw/master/src/main/docs/resources/img/checkstyle-issue.png) 75 | 76 | In this case, to fix the issues, we need to: 77 | 78 | - Add one empty line before `package` in line 16 79 | - Add the Javadoc for the constructor in line 27 80 | - Add an space after `if` in line 34 81 | 82 | The plugin also adds a new tab in the bottom of the IDE to run Checkstyle and show errors and warnings. We recommend that you run the report and fix all issues before submitting a pull request. 83 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme 2 | 3 | import io.micronaut.http.HttpRequest 4 | import io.micronaut.http.HttpResponse 5 | import io.micronaut.http.annotation.Controller 6 | import io.micronaut.http.annotation.Get 7 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 8 | import spock.lang.Stepwise 9 | import spock.util.concurrent.PollingConditions 10 | 11 | import javax.net.ssl.HttpsURLConnection 12 | import javax.net.ssl.SSLContext 13 | import java.security.SecureRandom 14 | import java.security.cert.Certificate 15 | import java.security.cert.X509Certificate 16 | 17 | @Stepwise 18 | class AcmeCertRefresherTaskSpec extends AcmeBaseSpec { 19 | 20 | Map getConfiguration(){ 21 | super.getConfiguration() << [ 22 | "acme.domains": EXPECTED_DOMAIN, 23 | ] 24 | } 25 | 26 | def "get new certificate using existing account"() { 27 | expect: 28 | new PollingConditions(timeout: 30).eventually { 29 | certFolder.list().length == 2 30 | certFolder.list().contains("domain.crt") 31 | certFolder.list().contains("domain.csr") 32 | } 33 | } 34 | 35 | void "expect the url to be https"() { 36 | expect: 37 | embeddedServer.getURL().toString() == "https://$EXPECTED_DOMAIN:$expectedSecurePort" 38 | } 39 | 40 | void "test certificate is one from pebble server"() { 41 | given: "we allow java to trust all certs since the test certs are not 100% valid" 42 | SSLContext sc = SSLContext.getInstance("SSL") 43 | sc.init(null, InsecureTrustManagerFactory.INSTANCE.trustManagers, new SecureRandom()) 44 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()) 45 | 46 | expect: "we get the cert that has been setup and we make sure they are from the pebble test server and the domain is as expected" 47 | new PollingConditions(timeout: 30).eventually { 48 | URL destinationURL = new URL(embeddedServer.getURL().toString() + "/ssl") 49 | HttpsURLConnection conn = (HttpsURLConnection) destinationURL.openConnection() 50 | try { 51 | conn.connect() 52 | Certificate[] certs = conn.getServerCertificates() 53 | certs.length == 2 54 | X509Certificate cert = certs[0] 55 | cert.getIssuerDN().getName().contains("Pebble Intermediate CA") 56 | cert.getSubjectDN().getName().contains(EXPECTED_DOMAIN) 57 | cert.getSubjectAlternativeNames().size() == 1 58 | 59 | X509Certificate cert2 = certs[1] 60 | cert2.issuerDN.name.contains("Pebble Root CA") 61 | cert2.subjectDN.name.contains("Pebble Intermediate CA") 62 | }finally{ 63 | if(conn != null){ 64 | conn.disconnect() 65 | } 66 | } 67 | } 68 | } 69 | 70 | void "test send https request when the cert is in place"() { 71 | when: 72 | HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/ssl"), String) 73 | 74 | then: 75 | response.body() == "Hello SSL" 76 | } 77 | 78 | @Controller('/') 79 | static class SslController { 80 | 81 | @Get('/ssl') 82 | String simple() { 83 | return "Hello SSL" 84 | } 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/challenges/AcmeCertRefresherTaskTlsApln01ChallengeSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme.challenges 2 | 3 | import io.micronaut.acme.AcmeBaseSpec 4 | import io.micronaut.http.HttpRequest 5 | import io.micronaut.http.HttpResponse 6 | import io.micronaut.http.annotation.Controller 7 | import io.micronaut.http.annotation.Get 8 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 9 | import spock.lang.Stepwise 10 | import spock.util.concurrent.PollingConditions 11 | 12 | import javax.net.ssl.HttpsURLConnection 13 | import javax.net.ssl.SSLContext 14 | import java.security.SecureRandom 15 | import java.security.cert.Certificate 16 | import java.security.cert.X509Certificate 17 | 18 | @Stepwise 19 | class AcmeCertRefresherTaskTlsApln01ChallengeSpec extends AcmeBaseSpec { 20 | Map getConfiguration(){ 21 | super.getConfiguration() << [ 22 | "acme.domains": EXPECTED_ACME_DOMAIN, 23 | "acme.challenge-type" : "tls" 24 | ] 25 | } 26 | 27 | @Override 28 | Map getPebbleEnv(){ 29 | return [ 30 | "PEBBLE_VA_ALWAYS_VALID": "0" 31 | ] 32 | } 33 | 34 | def "get new certificate using existing account"() { 35 | expect: 36 | new PollingConditions(timeout: 30).eventually { 37 | certFolder.list().length == 2 38 | certFolder.list().contains("domain.crt") 39 | certFolder.list().contains("domain.csr") 40 | } 41 | } 42 | 43 | void "expect the url to be https"() { 44 | expect: 45 | embeddedServer.getURL().toString() == "https://$EXPECTED_DOMAIN:$expectedSecurePort" 46 | } 47 | 48 | void "test certificate is one from pebble server"() { 49 | given: "we allow java to trust all certs since the test certs are not 100% valid" 50 | SSLContext sc = SSLContext.getInstance("SSL") 51 | sc.init(null, InsecureTrustManagerFactory.INSTANCE.trustManagers, new SecureRandom()) 52 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()) 53 | 54 | when: "we get the cert that has been setup" 55 | URL destinationURL = new URL(embeddedServer.getURL().toString() + "/tlschallenge") 56 | HttpsURLConnection conn = (HttpsURLConnection) destinationURL.openConnection() 57 | conn.connect() 58 | Certificate[] certs = conn.getServerCertificates() 59 | 60 | then: "we make sure they are from the pebble test server and the domain is as expected" 61 | certs.length == 2 62 | X509Certificate cert = certs[0] 63 | cert.getIssuerDN().getName().contains("Pebble Intermediate CA") 64 | cert.getSubjectDN().getName().contains(EXPECTED_ACME_DOMAIN) 65 | cert.getSubjectAlternativeNames().size() == 1 66 | 67 | X509Certificate cert2 = certs[1] 68 | cert2.issuerDN.name.contains("Pebble Root CA") 69 | cert2.subjectDN.name.contains("Pebble Intermediate CA") 70 | } 71 | 72 | void "test send https request when the cert is in place"() { 73 | when: 74 | HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/tlschallenge"), String) 75 | 76 | then: 77 | response.body() == "Hello TLS" 78 | } 79 | 80 | @Controller('/') 81 | static class SslController { 82 | 83 | @Get('/tlschallenge') 84 | String simple() { 85 | return "Hello TLS" 86 | } 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/challenges/AcmeCertRefresherTaskHttp01ChallengeSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme.challenges 2 | 3 | import io.micronaut.acme.AcmeBaseSpec 4 | import io.micronaut.http.HttpRequest 5 | import io.micronaut.http.HttpResponse 6 | import io.micronaut.http.annotation.Controller 7 | import io.micronaut.http.annotation.Get 8 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 9 | import spock.lang.Stepwise 10 | import spock.util.concurrent.PollingConditions 11 | 12 | import javax.net.ssl.HttpsURLConnection 13 | import javax.net.ssl.SSLContext 14 | import java.security.SecureRandom 15 | import java.security.cert.Certificate 16 | import java.security.cert.X509Certificate 17 | 18 | @Stepwise 19 | class AcmeCertRefresherTaskHttp01ChallengeSpec extends AcmeBaseSpec { 20 | Map getConfiguration(){ 21 | super.getConfiguration() << [ 22 | "acme.domains": EXPECTED_ACME_DOMAIN, 23 | "acme.challenge-type" : "http", 24 | "micronaut.server.dualProtocol": true, 25 | "micronaut.server.port" : expectedHttpPort 26 | ] 27 | } 28 | 29 | @Override 30 | Map getPebbleEnv(){ 31 | return [ 32 | "PEBBLE_VA_ALWAYS_VALID": "0" 33 | ] 34 | } 35 | 36 | def "get new certificate using existing account"() { 37 | expect: 38 | new PollingConditions(timeout: 30).eventually { 39 | certFolder.list().length == 2 40 | certFolder.list().contains("domain.crt") 41 | certFolder.list().contains("domain.csr") 42 | } 43 | } 44 | 45 | void "expect the url to be https"() { 46 | expect: 47 | embeddedServer.getURL().toString() == "https://$EXPECTED_DOMAIN:$expectedSecurePort" 48 | } 49 | 50 | void "test certificate is one from pebble server"() { 51 | given: "we allow java to trust all certs since the test certs are not 100% valid" 52 | SSLContext sc = SSLContext.getInstance("SSL") 53 | sc.init(null, InsecureTrustManagerFactory.INSTANCE.trustManagers, new SecureRandom()) 54 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()) 55 | 56 | when: "we get the cert that has been setup" 57 | URL destinationURL = new URL(embeddedServer.getURL().toString() + "/httpchallenge") 58 | HttpsURLConnection conn = (HttpsURLConnection) destinationURL.openConnection() 59 | conn.connect() 60 | Certificate[] certs = conn.getServerCertificates() 61 | 62 | then: "we make sure they are from the pebble test server and the domain is as expected" 63 | certs.length == 2 64 | X509Certificate cert = certs[0] 65 | cert.getIssuerDN().getName().contains("Pebble Intermediate CA") 66 | cert.getSubjectDN().getName().contains(EXPECTED_ACME_DOMAIN) 67 | cert.getSubjectAlternativeNames().size() == 1 68 | 69 | X509Certificate cert2 = certs[1] 70 | cert2.issuerDN.name.contains("Pebble Root CA") 71 | cert2.subjectDN.name.contains("Pebble Intermediate CA") 72 | } 73 | 74 | void "test send https request when the cert is in place"() { 75 | when: 76 | HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/httpchallenge"), String) 77 | 78 | then: 79 | response.body() == "Hello HTTP" 80 | } 81 | 82 | @Controller('/') 83 | static class SslController { 84 | 85 | @Get('/httpchallenge') 86 | String simple() { 87 | return "Hello HTTP" 88 | } 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherMultiDomainsTaskSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme 2 | 3 | import io.micronaut.http.HttpRequest 4 | import io.micronaut.http.HttpResponse 5 | import io.micronaut.http.annotation.Controller 6 | import io.micronaut.http.annotation.Get 7 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 8 | import spock.lang.Stepwise 9 | import spock.util.concurrent.PollingConditions 10 | 11 | import javax.net.ssl.HttpsURLConnection 12 | import javax.net.ssl.SSLContext 13 | import java.security.SecureRandom 14 | import java.security.cert.Certificate 15 | import java.security.cert.X509Certificate 16 | 17 | @Stepwise 18 | class AcmeCertRefresherMultiDomainsTaskSpec extends AcmeBaseSpec { 19 | 20 | Map getConfiguration(){ 21 | super.getConfiguration() << [ 22 | "acme.domains": "$EXPECTED_DOMAIN,$EXPECTED_ACME_DOMAIN", 23 | ] 24 | } 25 | 26 | def "get new certificate using existing account"() { 27 | expect: 28 | new PollingConditions(timeout: 30).eventually { 29 | certFolder.list().length == 2 30 | certFolder.list().contains("domain.crt") 31 | certFolder.list().contains("domain.csr") 32 | } 33 | } 34 | 35 | void "expect the url to be https"() { 36 | expect: 37 | embeddedServer.getURL().toString() == "https://$EXPECTED_DOMAIN:$expectedSecurePort" 38 | } 39 | 40 | void "test certificate is one from pebble server"() { 41 | given: "we allow java to trust all certs since the test certs are not 100% valid" 42 | SSLContext sc = SSLContext.getInstance("SSL") 43 | sc.init(null, InsecureTrustManagerFactory.INSTANCE.trustManagers, new SecureRandom()) 44 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()) 45 | 46 | expect: "we get the cert that has been setup and we make sure they are from the pebble test server and the domain is as expected" 47 | new PollingConditions(timeout: 30).eventually { 48 | URL destinationURL = new URL(embeddedServer.getURL().toString() + "/ssl") 49 | HttpsURLConnection conn = (HttpsURLConnection) destinationURL.openConnection() 50 | try { 51 | conn.connect() 52 | Certificate[] certs = conn.getServerCertificates() 53 | certs.length == 2 54 | X509Certificate cert = certs[0] 55 | cert.getIssuerDN().getName().contains("Pebble Intermediate CA") 56 | cert.getSubjectDN().getName().contains(EXPECTED_DOMAIN) 57 | cert.getSubjectAlternativeNames().size() == 2 58 | cert.getSubjectAlternativeNames().collect({d-> d.get(1)}).contains(EXPECTED_DOMAIN) 59 | cert.getSubjectAlternativeNames().collect({d-> d.get(1)}).contains(EXPECTED_ACME_DOMAIN) 60 | 61 | X509Certificate cert2 = certs[1] 62 | cert2.issuerDN.name.contains("Pebble Root CA") 63 | cert2.subjectDN.name.contains("Pebble Intermediate CA") 64 | }finally{ 65 | if(conn != null){ 66 | conn.disconnect() 67 | } 68 | } 69 | } 70 | } 71 | 72 | void "test send https request when the cert is in place"() { 73 | when: 74 | HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/ssl-multidomains"), String) 75 | 76 | then: 77 | response.body() == "Hello There" 78 | } 79 | 80 | @Controller('/') 81 | static class SslController { 82 | 83 | @Get('/ssl-multidomains') 84 | String simple() { 85 | return "Hello There" 86 | } 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/hello-world-acme/README.md: -------------------------------------------------------------------------------- 1 | # Hello world acme 2 | This is a simple micronaut app that has been configured 90% of the way that should help you test out an actual implementation. 3 | It contains a single endpoint found at `/helloWorld` but the important bits can be found in [src/main/resources/application.yml](src/main/resources/application.yml). 4 | 5 | ## Steps to get up and running 6 | 7 | ### Pre-reqs 8 | 1. You have created an account with Let's Encrypt 9 | 1. You have generated a domain key 10 | 1. microanut-cli (aka micronaut-starter) can help with steps 1 and 2 11 | 1. You have purchased a domain name and have a way to configure DNS. In the AWS example below we will use Route53. 12 | 13 | ### Build and Deploy : 14 | 1. From inside the `hello-world-acme` project execute the following 15 | 1. `../../gradlew build` 16 | 17 | ### AWS only example : 18 | 1. Launch an EC2 instance with >= java 8 installed 19 | 1. Tested with `amzn2-ami-hvm-2.0.20190612-x86_64-gp2 (ami-00c79db59589996b9)` but anything with >= java 8 installed should work 20 | 1. Configure EC2 Security Group to allow SSH port 22 and HTTPS port 443 traffic through 21 | 1. If using HTTP challenge also allow port 80 through 22 | ![Configure Security Group](docs/images/EC2_Security_Group.png) 23 | 1. Configure Route 53 DNS to point to your EC2 public ip address 24 | 1. First to get the IP address of your EC2 instance 25 | ![Obtain IPv4 IP Address for EC2 instance](docs/images/Instances__EC2_Management_Console.png) 26 | 1. Next create a new record set in Route 53 using the public ipv4 address for your EC2 server 27 | ![Create A record in Route53](docs/images/Route_53_Management_Console.png) 28 | 1. scp the `hello-world-acme` jar found here `examples/hello-world-acme/build/libs/acme-example-hello-world-acme-1.0.0.BUILD-SNAPSHOT-all.jar` to your EC2 server 29 | 1. Example command might look something like this if using the terminal 30 | 1. `scp -i ~/aws-keypair.pem examples/hello-world-acme/build/libs/acme-example-hello-world-acme-1.0.0.BUILD-SNAPSHOT-all.jar ec2-user@ec2-52-15-231-234.us-east-2.compute.amazonaws.com:~` 31 | 1. ssh into your EC2 server 32 | 1. Example command might look something like this if using the terminal 33 | 1. `ssh -i ~/aws-keypair.pem ec2-user@ec2-52-15-231-234.us-east-2.compute.amazonaws.com` 34 | 1. Setup your environment 35 | 1. You will need to define the following environment variables or in this case environment yaml to successfully start the application. Using environment yaml/variables keeps you from committing 36 | your private key into source control. Define the properties you want to override in this yaml. Usually this would be domain, account and domain key. But also could include anything else you would like to override. 37 | 1. Create an `env.yml` file on your ec2 server 38 | ``` 39 | domains: 40 | - 41 | domain-key: | 42 | -----BEGIN RSA PRIVATE KEY----- 43 | 44 | -----END RSA PRIVATE KEY----- 45 | account-key: | 46 | -----BEGIN RSA PRIVATE KEY----- 47 | 48 | -----END RSA PRIVATE KEY----- 49 | ``` 50 | 1. Start the application 51 | 1. Note this will require sudo since we are running on 80/443. This is not ideal in a production environment and some form of load balancer 52 | should generally be used to accept traffic on 80/443 and forward to generally 8080/8443 (default micronaut ports). 53 | 1. `sudo java -Dmicronaut.config.files=env.yml -Dmicronaut.environments= -jar acme-example-hello-world-acme-1.0.0.BUILD-SNAPSHOT-all.jar` 54 | 1. Celebrate 55 | 1. You should now see something like this when you navigate to `https:///helloWorld` 56 | 1. ![Secured site with Let's Encrypt certificate](docs/images/Acme_cert_micronaut.png) 57 | 58 | ### GCP example : 59 | //TODO 60 | 61 | ### Azure example : 62 | //TODO -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskUnitSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme 2 | 3 | import io.micronaut.acme.background.AcmeCertRefresherTask 4 | import io.micronaut.acme.services.AcmeService 5 | import io.micronaut.runtime.EmbeddedApplication 6 | import io.micronaut.runtime.event.ApplicationStartupEvent 7 | import io.micronaut.runtime.exceptions.ApplicationStartupException 8 | import io.netty.handler.ssl.util.SelfSignedCertificate 9 | import org.shredzone.acme4j.exception.AcmeException 10 | import spock.lang.Specification 11 | import spock.lang.Stepwise 12 | import spock.lang.Unroll 13 | 14 | import java.time.Duration 15 | 16 | @Stepwise 17 | class AcmeCertRefresherTaskUnitSpec extends Specification { 18 | 19 | def "throw exception if TOS has not been accepted"() { 20 | given: 21 | def task = new AcmeCertRefresherTask(Mock(AcmeService), Mock(AcmeConfiguration)) 22 | 23 | when: 24 | task.renewCertIfNeeded() 25 | 26 | then: 27 | def ex = thrown(IllegalStateException.class) 28 | ex.message == "Cannot refresh certificates until terms of service is accepted. Please review the TOS for Let's Encrypt and set \"acme.tos-agree\" to \"true\" in configuration once complete" 29 | } 30 | 31 | def "if certificate is greater than renew time we do nothing"() { 32 | given: 33 | def expectedDomain = "example.com" 34 | AcmeConfiguration config = new AcmeConfiguration(tosAgree: true, domains: [expectedDomain], renewWitin: Duration.ofDays(30)) 35 | def mockAcmeSerivce = Mock(AcmeService) 36 | 37 | def task = new AcmeCertRefresherTask(mockAcmeSerivce, config) 38 | 39 | when: 40 | task.renewCertIfNeeded() 41 | 42 | then: 43 | 1 * mockAcmeSerivce.getCurrentCertificate() >> new SelfSignedCertificate(expectedDomain, new Date(), new Date() + 31).cert() 44 | 0 * mockAcmeSerivce.orderCertificate([expectedDomain]) 45 | 46 | } 47 | 48 | @Unroll 49 | def "if certificate is #description we order a new certificate"() { 50 | given: 51 | def mockAcmeSerivce = Mock(AcmeService) 52 | String expectedDomain = "example.com" 53 | AcmeConfiguration config = new AcmeConfiguration(tosAgree: true, domains: [expectedDomain], renewWitin: Duration.ofDays(daysToRenew)) 54 | def task = new AcmeCertRefresherTask(mockAcmeSerivce, config) 55 | 56 | when: 57 | task.renewCertIfNeeded() 58 | 59 | then: 60 | 1 * mockAcmeSerivce.getCurrentCertificate() >> new SelfSignedCertificate(expectedDomain, new Date(), new Date() + 31).cert() 61 | 1 * mockAcmeSerivce.orderCertificate([expectedDomain]) 62 | 63 | where: 64 | daysToRenew | description 65 | 31 | "equal to renew days" 66 | 35 | "less than renew days" 67 | 68 | } 69 | 70 | def "if acme service fails on app start up to do anything the app wont start since SSL will be hosed anyways"(){ 71 | given: 72 | def mockAcmeSerivce = Mock(AcmeService) 73 | def expectedDomains = ["example.com"] 74 | AcmeConfiguration config = new AcmeConfiguration(tosAgree: true, domains: expectedDomains, renewWitin: Duration.ofDays(100)) 75 | def task = new AcmeCertRefresherTask(mockAcmeSerivce, config) 76 | 77 | when: 78 | task.onStartup(new ApplicationStartupEvent(Mock(EmbeddedApplication))) 79 | 80 | then: 81 | def ex = thrown(ApplicationStartupException) 82 | ex.message == "Failed to start due to SSL configuration issue." 83 | 84 | and: 85 | 1 * mockAcmeSerivce.getCurrentCertificate() >> null 86 | 1 * mockAcmeSerivce.orderCertificate(expectedDomains) >> { List domains -> 87 | throw new AcmeException("Failed to do some ACME related task") 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskWithFileKeysSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme 2 | 3 | import io.micronaut.http.HttpRequest 4 | import io.micronaut.http.HttpResponse 5 | import io.micronaut.http.annotation.Controller 6 | import io.micronaut.http.annotation.Get 7 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 8 | import spock.lang.Stepwise 9 | import spock.util.concurrent.PollingConditions 10 | 11 | import javax.net.ssl.HttpsURLConnection 12 | import javax.net.ssl.SSLContext 13 | import java.security.SecureRandom 14 | import java.security.cert.Certificate 15 | import java.security.cert.X509Certificate 16 | 17 | @Stepwise 18 | class AcmeCertRefresherTaskWithFileKeysSpec extends AcmeBaseSpec { 19 | 20 | Map getConfiguration(){ 21 | def accountKeyFile = File.createTempFile("account", "key") 22 | accountKeyFile.write(accountKey) 23 | 24 | def domainKeyFile = File.createTempFile("domain", "key") 25 | domainKeyFile.write(domainKey) 26 | 27 | 28 | super.getConfiguration() << [ 29 | "acme.domains": EXPECTED_DOMAIN, 30 | "acme.domain-key": "file:${domainKeyFile.toPath()}", 31 | "acme.account-key": "file:${accountKeyFile.toPath()}" 32 | ] 33 | } 34 | 35 | def "get new certificate using existing account"() { 36 | expect: 37 | new PollingConditions(timeout: 30).eventually { 38 | certFolder.list().length == 2 39 | certFolder.list().contains("domain.crt") 40 | certFolder.list().contains("domain.csr") 41 | } 42 | } 43 | 44 | void "expect the url to be https"() { 45 | expect: 46 | embeddedServer.getURL().toString() == "https://$EXPECTED_DOMAIN:$expectedSecurePort" 47 | } 48 | 49 | void "test certificate is one from pebble server"() { 50 | given: "we allow java to trust all certs since the test certs are not 100% valid" 51 | SSLContext sc = SSLContext.getInstance("SSL") 52 | sc.init(null, InsecureTrustManagerFactory.INSTANCE.trustManagers, new SecureRandom()) 53 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()) 54 | 55 | expect: "we get the cert that has been setup and we make sure they are from the pebble test server and the domain is as expected" 56 | new PollingConditions(timeout: 30).eventually { 57 | URL destinationURL = new URL(embeddedServer.getURL().toString() + "/ssl") 58 | HttpsURLConnection conn = (HttpsURLConnection) destinationURL.openConnection() 59 | try { 60 | conn.connect() 61 | Certificate[] certs = conn.getServerCertificates() 62 | certs.length == 2 63 | X509Certificate cert = certs[0] 64 | cert.getIssuerDN().getName().contains("Pebble Intermediate CA") 65 | cert.getSubjectDN().getName().contains(EXPECTED_DOMAIN) 66 | cert.getSubjectAlternativeNames().size() == 1 67 | 68 | X509Certificate cert2 = certs[1] 69 | cert2.issuerDN.name.contains("Pebble Root CA") 70 | cert2.subjectDN.name.contains("Pebble Intermediate CA") 71 | }finally{ 72 | if(conn != null){ 73 | conn.disconnect() 74 | } 75 | } 76 | } 77 | } 78 | 79 | void "test send https request when the cert is in place"() { 80 | when: 81 | HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/ssl-using-file-keys"), String) 82 | 83 | then: 84 | response.body() == "Hello File" 85 | } 86 | 87 | @Controller('/') 88 | static class SslController { 89 | 90 | @Get('/ssl-using-file-keys') 91 | String simple() { 92 | return "Hello File" 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/AcmeCertWildcardRefresherTaskSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme 2 | 3 | import io.micronaut.http.HttpRequest 4 | import io.micronaut.http.HttpResponse 5 | import io.micronaut.http.annotation.Controller 6 | import io.micronaut.http.annotation.Get 7 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 8 | import spock.lang.Stepwise 9 | import spock.util.concurrent.PollingConditions 10 | 11 | import javax.net.ssl.HttpsURLConnection 12 | import javax.net.ssl.SSLContext 13 | import java.security.SecureRandom 14 | import java.security.cert.Certificate 15 | import java.security.cert.X509Certificate 16 | 17 | @Stepwise 18 | class AcmeCertWildcardRefresherTaskSpec extends AcmeBaseSpec { 19 | 20 | public static final String EXPECTED_BASE_DOMAIN = "localhost" 21 | public static final String EXPECTED_DOMAIN = EXPECTED_BASE_DOMAIN 22 | public static final String WILDCARD_DOMAIN = "*.${EXPECTED_BASE_DOMAIN}".toString() 23 | 24 | Map getConfiguration(){ 25 | super.getConfiguration() << [ 26 | "acme.domains": WILDCARD_DOMAIN, 27 | "acme.challenge-type" : "dns" 28 | ] 29 | } 30 | 31 | def "get new certificate using existing account"() { 32 | expect: 33 | new PollingConditions(timeout: 30).eventually { 34 | certFolder.list().length == 2 35 | certFolder.list().contains("domain.crt") 36 | certFolder.list().contains("domain.csr") 37 | } 38 | } 39 | 40 | void "expect the url to be https"() { 41 | expect: 42 | embeddedServer.getURL().toString() == "https://$EXPECTED_DOMAIN:$expectedSecurePort" 43 | } 44 | 45 | void "test certificate is one from pebble server"() { 46 | given: "we allow java to trust all certs since the test certs are not 100% valid" 47 | SSLContext sc = SSLContext.getInstance("SSL") 48 | sc.init(null, InsecureTrustManagerFactory.INSTANCE.trustManagers, new SecureRandom()) 49 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()) 50 | 51 | expect: "we get the cert that has been setup and we make sure they are from the pebble test server and the domain is as expected" 52 | new PollingConditions(timeout: 30).eventually { 53 | URL destinationURL = new URL(embeddedServer.getURL().toString() + "/wildcardssl") 54 | HttpsURLConnection conn = (HttpsURLConnection) destinationURL.openConnection() 55 | try { 56 | conn.connect() 57 | Certificate[] certs = conn.getServerCertificates() 58 | certs.length == 2 59 | X509Certificate cert = certs[0] 60 | cert.getIssuerDN().getName().contains("Pebble Intermediate CA") 61 | cert.getSubjectDN().getName().contains(WILDCARD_DOMAIN) 62 | cert.getSubjectAlternativeNames().size() == 2 63 | cert.getSubjectAlternativeNames().collect({d-> d.get(1)}).contains(WILDCARD_DOMAIN) 64 | cert.getSubjectAlternativeNames().collect({d-> d.get(1)}).contains(EXPECTED_BASE_DOMAIN) 65 | 66 | X509Certificate cert2 = certs[1] 67 | cert2.issuerDN.name.contains("Pebble Root CA") 68 | cert2.subjectDN.name.contains("Pebble Intermediate CA") 69 | }finally{ 70 | if(conn != null){ 71 | conn.disconnect() 72 | } 73 | } 74 | } 75 | } 76 | 77 | void "test send https request when the cert is in place"() { 78 | when: 79 | HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/wildcardssl"), String) 80 | 81 | then: 82 | response.body() == "Hello Wildcard" 83 | } 84 | 85 | @Controller('/') 86 | static class SslController { 87 | 88 | @Get('/wildcardssl') 89 | String simple() { 90 | return "Hello Wildcard" 91 | } 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/docs/guide/configuration.adoc: -------------------------------------------------------------------------------- 1 | Micronaut 1.3.0 or above is required and you must have the `micronaut-acme` dependency on your classpath: 2 | 3 | dependency:micronaut-acme[groupId="io.micronaut.acme"] 4 | 5 | The `micronaut-acme` module transitively includes the `org.shredzone.acme4j:acme4j-client` and `org.shredzone.acme4j:acme4j-utils` dependency. 6 | 7 | .src/main/resources/application.yml 8 | [source,yaml] 9 | ---- 10 | micronaut: 11 | server: 12 | port : 80 //<1> 13 | dual-protocol: true //<2> 14 | ssl: 15 | enabled: true // <3> 16 | acme: 17 | enabled: true // <4> 18 | tos-agree: true // <5> 19 | cert-location: /path/to/store/certificates // <6> 20 | domains: //<7> 21 | - stage.domain.com 22 | - test.domain.com 23 | refresh: 24 | delay: 1m // <8> 25 | frequency: 24h // <9> 26 | domain-key: | // <10> 27 | -----BEGIN RSA PRIVATE KEY----- 28 | MIIEowIBAAKCAQEAi32GgrNvt5sYonmvFRs1lYMdUTsoFHz33knzsTvBRb+S1JCc 29 | al86zAx3dRdFiLyWw4/lXmS6oS5B/NT1w9R7nW3vd0oi4ump/QjWjOd8SxCBqMcR 30 | .... 31 | MIIEowIBAAKCAQEAi32GgrNvt5sYonmvFRs1lYMdUTsoFHz33knzsTvBRb+S1JCc 32 | al86zAx3dRdFiLyWw4/lXmS6oS5B/NT1w9R7nW3vd0oi4ump/QjWjOd8SxCBqMcR 33 | -----END RSA PRIVATE KEY----- 34 | account-key: | // <11> 35 | -----BEGIN RSA PRIVATE KEY----- 36 | MIIEowIBAAKCAQEAi32GgrNvt5sYonmvFRs1lYMdUTsoFHz33knzsTvBRb+S1JCc 37 | al86zAx3dRdFiLyWw4/lXmS6oS5B/NT1w9R7nW3vd0oi4ump/QjWjOd8SxCBqMcR 38 | .... 39 | MIIEowIBAAKCAQEAi32GgrNvt5sYonmvFRs1lYMdUTsoFHz33knzsTvBRb+S1JCc 40 | al86zAx3dRdFiLyWw4/lXmS6oS5B/NT1w9R7nW3vd0oi4ump/QjWjOd8SxCBqMcR 41 | -----END RSA PRIVATE KEY----- 42 | acme-server: acme://server.com // <12> 43 | order: 44 | pause: 3s // <13> 45 | refresh-attempts: 10 // <14> 46 | auth: 47 | pause: 1m // <15> 48 | refresh-attempts: 10 // <16> 49 | renew-within: 30 // <17> 50 | challenge-type: tls // <18> 51 | timeout: 10s //<19> 52 | ---- 53 | <1> Set the http port for micronaut. If using http challenge-type this must be set to port 80, unless using a load balancer or some other proxy as Let's Encrypt for example only sends request to port 80. 54 | <2> Enables dual port mode that allows for both http and https to be bound. Default is `false` 55 | <3> Enables ssl for micronaut. Default is `false` 56 | <4> Enables ACME integration for micronaut. Default is `false` 57 | <5> Agrees to the Terms of Service of the ACME provider. Default is `false` 58 | <6> Location to store the certificate on the server. 59 | <7> Domain name(s) for the certificate. Can be a 1 or many domains or even a wildcard domain. 60 | <8> How long to wait until the server starts up the ACME background process. Default is `24 hours` 61 | <9> How often the server will check for a new ACME cert and refresh it if needed. Default is `24 hours` 62 | <10> Private key used to encrypt the certificate. Other options you can use here are `classpath:/path/to/key.pem` or `file:/path/to/key.pem`. It is advisable to not check this into source control as this is the secret to handle the domain encryption. 63 | <11> Private key used to when setting up your account with the ACME provider. Other options you can use here are `classpath:/path/to/key.pem` or `file:/path/to/key.pem`. It is advisable to not check this into source control as this is your account identifier. 64 | <12> Url of the ACME server (ex. acme://letsencrypt.org/staging) 65 | <13> Time to wait in between polling order status of the ACME server. Default is `3 seconds` 66 | <14> Number of times to poll an order status of the ACME server. Default is `10` 67 | <15> Time to wait in between polling authorization status of the ACME server. Default is `3 seconds` 68 | <16> Number of times to poll an authorization status of the ACME server. Default is `10` 69 | <17> Number of days before the process will start to try to refresh the certificate from the ACME provider. Default is `30 days` 70 | <18> The challenge type you would like to use. Default is `tls`. Possible options : http, tls, dns 71 | <19> Sets the connection/read timeout when making http calls to the ACME server. Default comes from here https://shredzone.org/maven/acme4j/acme4j-client/apidocs/src-html/org/shredzone/acme4j/connector/NetworkSettings.html#line.61 72 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskWithClasspathKeysSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme 2 | 3 | import io.micronaut.http.HttpRequest 4 | import io.micronaut.http.HttpResponse 5 | import io.micronaut.http.annotation.Controller 6 | import io.micronaut.http.annotation.Get 7 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 8 | import org.shredzone.acme4j.util.KeyPairUtils 9 | import spock.lang.Stepwise 10 | import spock.util.concurrent.PollingConditions 11 | 12 | import javax.net.ssl.HttpsURLConnection 13 | import javax.net.ssl.SSLContext 14 | import java.security.KeyPair 15 | import java.security.SecureRandom 16 | import java.security.cert.Certificate 17 | import java.security.cert.X509Certificate 18 | 19 | @Stepwise 20 | class AcmeCertRefresherTaskWithClasspathKeysSpec extends AcmeBaseSpec { 21 | 22 | @Override 23 | KeyPair getDomainKeypair() { 24 | KeyPairUtils.readKeyPair(new InputStreamReader(this.getClass().getResourceAsStream("/test-domain.pem"))) 25 | } 26 | 27 | @Override 28 | KeyPair getAccountKeypair() { 29 | KeyPairUtils.readKeyPair(new InputStreamReader(this.getClass().getResourceAsStream("/test-account.pem"))) 30 | } 31 | 32 | @Override 33 | Map getConfiguration(){ 34 | super.getConfiguration() << [ 35 | "acme.domains": EXPECTED_DOMAIN, 36 | "acme.domain-key": "classpath:test-domain.pem", 37 | "acme.account-key": "classpath:test-account.pem" 38 | ] 39 | } 40 | 41 | def "get new certificate using existing account"() { 42 | expect: 43 | new PollingConditions(timeout: 30).eventually { 44 | certFolder.list().length == 2 45 | certFolder.list().contains("domain.crt") 46 | certFolder.list().contains("domain.csr") 47 | } 48 | } 49 | 50 | void "expect the url to be https"() { 51 | expect: 52 | embeddedServer.getURL().toString() == "https://$EXPECTED_DOMAIN:$expectedSecurePort" 53 | } 54 | 55 | void "test certificate is one from pebble server"() { 56 | given: "we allow java to trust all certs since the test certs are not 100% valid" 57 | SSLContext sc = SSLContext.getInstance("SSL") 58 | sc.init(null, InsecureTrustManagerFactory.INSTANCE.trustManagers, new SecureRandom()) 59 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()) 60 | 61 | expect: "we get the cert that has been setup and we make sure they are from the pebble test server and the domain is as expected" 62 | new PollingConditions(timeout: 30).eventually { 63 | URL destinationURL = new URL(embeddedServer.getURL().toString() + "/ssl") 64 | HttpsURLConnection conn = (HttpsURLConnection) destinationURL.openConnection() 65 | try { 66 | conn.connect() 67 | Certificate[] certs = conn.getServerCertificates() 68 | certs.length == 2 69 | X509Certificate cert = certs[0] 70 | cert.getIssuerDN().getName().contains("Pebble Intermediate CA") 71 | cert.getSubjectDN().getName().contains(EXPECTED_DOMAIN) 72 | cert.getSubjectAlternativeNames().size() == 1 73 | 74 | X509Certificate cert2 = certs[1] 75 | cert2.issuerDN.name.contains("Pebble Root CA") 76 | cert2.subjectDN.name.contains("Pebble Intermediate CA") 77 | }finally{ 78 | if(conn != null){ 79 | conn.disconnect() 80 | } 81 | } 82 | } 83 | } 84 | 85 | void "test send https request when the cert is in place"() { 86 | when: 87 | HttpResponse response = client.toBlocking().exchange(HttpRequest.GET("/ssl-using-classpath-keys"), String) 88 | 89 | then: 90 | response.body() == "Hello Classpath" 91 | } 92 | 93 | @Controller('/') 94 | static class SslController { 95 | 96 | @Get('/ssl-using-classpath-keys') 97 | String simple() { 98 | return "Hello Classpath" 99 | } 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/ssl/AcmeSSLContextBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micronaut.acme.ssl; 17 | 18 | import io.micronaut.acme.events.CertificateEvent; 19 | import io.micronaut.context.annotation.Replaces; 20 | import io.micronaut.http.server.netty.ssl.CertificateProvidedSslBuilder; 21 | import io.micronaut.http.server.netty.ssl.ServerSslBuilder; 22 | import io.micronaut.http.ssl.ServerSslConfiguration; 23 | import io.micronaut.runtime.event.annotation.EventListener; 24 | import io.netty.handler.ssl.*; 25 | import jakarta.inject.Singleton; 26 | import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import javax.net.ssl.SSLException; 31 | import java.util.Optional; 32 | 33 | /** 34 | * The Netty implementation of {@link ServerSslBuilder} that generates an {@link SslContext} to create a server handler 35 | * with to SSL support via a temporary self signed certificate that will be replaced by an ACME certificate once acquired. 36 | */ 37 | @Singleton 38 | @Replaces(CertificateProvidedSslBuilder.class) 39 | public class AcmeSSLContextBuilder implements ServerSslBuilder { 40 | 41 | private static final Logger LOG = LoggerFactory.getLogger(AcmeSSLContextBuilder.class); 42 | 43 | private DelegatedSslContext delegatedSslContext = new DelegatedSslContext(null); 44 | private final ServerSslConfiguration ssl; 45 | 46 | /** 47 | * @param ssl The SSL configuration 48 | */ 49 | public AcmeSSLContextBuilder(ServerSslConfiguration ssl) { 50 | this.ssl = ssl; 51 | } 52 | 53 | /** 54 | * Listens for CertificateEvent containing the ACME certificate and replaces the {@link SslContext} to now use that certificate. 55 | * 56 | * @param certificateEvent {@link CertificateEvent} 57 | */ 58 | @EventListener 59 | void onNewCertificate(CertificateEvent certificateEvent) { 60 | try { 61 | if (LOG.isDebugEnabled()) { 62 | LOG.debug("New certificate received and replaced the proxied SSL context"); 63 | } 64 | if (certificateEvent.isValidationCert()) { 65 | SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK; 66 | SslContext sslContext = SslContextBuilder 67 | .forServer(certificateEvent.getDomainKeyPair().getPrivate(), certificateEvent.getCert()) 68 | .sslProvider(provider) 69 | .applicationProtocolConfig(new ApplicationProtocolConfig( 70 | ApplicationProtocolConfig.Protocol.ALPN, 71 | // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. 72 | ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, 73 | // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. 74 | ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, 75 | TlsAlpn01Challenge.ACME_TLS_1_PROTOCOL)) 76 | .build(); 77 | delegatedSslContext.setNewSslContext(sslContext); 78 | } else { 79 | SslContext sslContext = SslContextBuilder 80 | .forServer(certificateEvent.getDomainKeyPair().getPrivate(), certificateEvent.getFullCertificateChain()) 81 | .build(); 82 | delegatedSslContext.setNewSslContext(sslContext); 83 | } 84 | } catch (SSLException e) { 85 | if (LOG.isErrorEnabled()) { 86 | LOG.error("Failed to build the SSL context", e); 87 | } 88 | } 89 | } 90 | 91 | @Override 92 | public ServerSslConfiguration getSslConfiguration() { 93 | return ssl; 94 | } 95 | 96 | /** 97 | * Generates an SslContext that has an already expired self signed cert that should be replaced almost immediately by the ACME server once it is downloaded. 98 | * 99 | * @return Optional SslContext 100 | */ 101 | @Override 102 | public Optional build() { 103 | return Optional.of(delegatedSslContext); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/docs/guide/cli/usage.adoc: -------------------------------------------------------------------------------- 1 | To use these functions you must first enable the `acme` feature in your app. 2 | 3 | == For a new app 4 | Either at creation time you will need to select the `acme` feature 5 | 6 | Using the Micronaut CLI select the `acme` feature on creation. 7 | 8 | [source,bash] 9 | ---- 10 | mn create-app --features acme hello-world 11 | ---- 12 | 13 | Or using Micronaut Launch https://micronaut.io/launch/ simply select `acme` feature before downloading your pre-built app. 14 | 15 | == For an existing app 16 | Use the micronaut cli to do a `feature-diff` on an exiting app to show the changes needed 17 | to enable the feature. 18 | 19 | ex. CLI Feature Diff 20 | [source,bash] 21 | ---- 22 | cd 23 | mn feature-diff --features acme 24 | ---- 25 | 26 | == Creating keypairs 27 | 28 | A utility to help with creating keypairs. This is akin to doing something like so with openssl 29 | 30 | [source,bash] 31 | ---- 32 | $ openssl genrsa -out /tmp/mydomain.com-key.pem 4096 33 | ---- 34 | 35 | These keypairs will be used for both ACME accounts as well as each domain will also need its own keypair defined. 36 | 37 | Usage: 38 | 39 | [source,bash] 40 | ---- 41 | Usage: mn create-key [-fhvVx] [-k=] -n= [-s=] 42 | Creates an keypair for use with ACME integration 43 | -f, --force Whether to overwrite existing files 44 | -h, --help Show this help message and exit. 45 | -k, --key-dir= Custom location on disk to put the key to be used 46 | with this account. 47 | Default: src/main/resources 48 | -n, --key-name= Name of the key to be created 49 | -s, --key-size= Size of the key to be generated 50 | Default: 4096 51 | -v, --verbose Create verbose output. 52 | -V, --version Print version information and exit. 53 | -x, --stacktrace Show full stack trace when exceptions occur. 54 | ---- 55 | 56 | == Creating an Account 57 | 58 | Creates a new account for a given ACME provider. This command will either create a new account keypair for you or you can pass 59 | the account keypair that you have generated using the `mn create-key` or via `openssl` or other means in as a parameter. 60 | 61 | https://certbot.eff.org/[Certbot] or many of the other tools out there can also accomplish this step if you dont want to use this tool. 62 | 63 | Usage: 64 | 65 | [source,bash] 66 | ---- 67 | Usage: mn create-acme-account (-u= | --lets-encrypt-prod | --lets-encrypt-staging) 68 | [-fhvVx] -e= [-k=] -n= [-s=] 69 | Creates a new account on the given ACME server 70 | -e, --email= Email address to create account with. 71 | -f, --force Whether to overwrite existing files 72 | -h, --help Show this help message and exit. 73 | -k, --key-dir= Custom location on disk to put the key to be used with this 74 | account. 75 | Default: src/main/resources 76 | -n, --key-name= Name of the key to be created 77 | -s, --key-size= Size of the key to be generated 78 | Default: 4096 79 | -v, --verbose Create verbose output. 80 | -V, --version Print version information and exit. 81 | -x, --stacktrace Show full stack trace when exceptions occur. 82 | ACME server URL 83 | --lets-encrypt-prod Use the Let's Encrypt prod URL. 84 | --lets-encrypt-staging Use the Let's Encrypt staging URL 85 | -u, --url= URL of ACME server to use 86 | ---- 87 | 88 | == Deactivating an Account 89 | 90 | Deactivates a given account based on the account key that was used to create the account. 91 | 92 | Usage: 93 | 94 | [source,bash] 95 | ---- 96 | Usage: mn deactivate-acme-account (-u= | --lets-encrypt-prod | 97 | --lets-encrypt-staging) [-fhvVx] [-k=] [-n=] 98 | Deactivates an existing ACME account 99 | -f, --force Whether to overwrite existing files 100 | -h, --help Show this help message and exit. 101 | -k, --key-dir= Directory to find the key to be used for this account. 102 | Default: src/main/resources 103 | -n, --key-name= Name of the key to be used 104 | Default: null 105 | -v, --verbose Create verbose output. 106 | -V, --version Print version information and exit. 107 | -x, --stacktrace Show full stack trace when exceptions occur. 108 | ACME server URL 109 | --lets-encrypt-prod Use the Let's Encrypt prod URL. 110 | --lets-encrypt-staging Use the Let's Encrypt staging URL 111 | -u, --url= URL of ACME server to use 112 | ---- 113 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Do not edit this file directly. Instead, go to: 2 | # 3 | # https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows 4 | # 5 | # and edit them there. Note that it will be sync'ed to all the Micronaut repos 6 | name: Java CI 7 | on: 8 | push: 9 | branches: 10 | - master 11 | - '[0-9]+.[0-9]+.x' 12 | pull_request: 13 | branches: 14 | - master 15 | - '[0-9]+.[0-9]+.x' 16 | jobs: 17 | build: 18 | if: github.repository != 'micronaut-projects/micronaut-project-template' 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | java: ['21', '25'] 23 | env: 24 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 25 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 26 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 27 | GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} 28 | GH_USERNAME: ${{ secrets.GH_USERNAME }} 29 | TESTCONTAINERS_RYUK_DISABLED: true 30 | PREDICTIVE_TEST_SELECTION: "${{ github.event_name == 'pull_request' && 'true' || 'false' }}" 31 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | OSS_INDEX_USERNAME: ${{ secrets.OSS_INDEX_USERNAME }} 34 | OSS_INDEX_PASSWORD: ${{ secrets.OSS_INDEX_PASSWORD }} 35 | steps: 36 | # https://github.com/actions/virtual-environments/issues/709 37 | - name: Remove system JDKs 38 | run: | 39 | sudo rm -rf /usr/lib/jvm/* 40 | unset JAVA_HOME 41 | export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '/usr/lib/jvm' | paste -sd:) 42 | - name: "🗑 Free disk space" 43 | run: | 44 | sudo rm -rf "/usr/local/share/boost" 45 | sudo rm -rf "$AGENT_TOOLSDIRECTORY" 46 | sudo apt-get clean 47 | df -h 48 | 49 | - name: "📥 Checkout repository" 50 | uses: actions/checkout@v6 51 | with: 52 | fetch-depth: 0 53 | 54 | - name: "🔧 Setup GraalVM CE" 55 | uses: graalvm/setup-graalvm@v1.4.4 56 | with: 57 | distribution: 'graalvm' 58 | java-version: ${{ matrix.java }} 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | - name: "🔧 Setup Gradle" 62 | uses: gradle/actions/setup-gradle@v5 63 | 64 | - name: "❓ Optional setup step" 65 | run: | 66 | [ -f ./setup.sh ] && ./setup.sh || [ ! -f ./setup.sh ] 67 | 68 | - name: "🚔 Sonatype Scan" 69 | if: env.OSS_INDEX_PASSWORD != '' && matrix.java == '21' 70 | id: sonatypescan 71 | run: | 72 | ./gradlew ossIndexAudit --no-parallel --info 73 | 74 | - name: "🛠 Build with Gradle" 75 | id: gradle 76 | run: | 77 | ./gradlew check jacocoReport --no-daemon --continue 78 | 79 | - name: "🔎 Run static analysis" 80 | if: env.SONAR_TOKEN != '' && matrix.java == '21' 81 | run: | 82 | ./gradlew sonar --no-parallel --continue 83 | 84 | - name: "📊 Publish Test Report" 85 | if: always() 86 | uses: mikepenz/action-junit-report@v6 87 | with: 88 | check_name: Java CI / Test Report (${{ matrix.java }}) 89 | report_paths: '**/build/test-results/test/TEST-*.xml' 90 | check_retries: 'true' 91 | 92 | - name: "📜 Upload binary compatibility check results" 93 | if: matrix.java == '21' 94 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 95 | with: 96 | name: binary-compatibility-reports 97 | path: "**/build/reports/binary-compatibility-*.html" 98 | 99 | - name: "📦 Publish to Sonatype Snapshots" 100 | if: success() && github.event_name == 'push' && matrix.java == '21' 101 | env: 102 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 103 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 104 | run: ./gradlew publishToSonatype docs --no-daemon 105 | 106 | - name: "❓ Determine docs target repository" 107 | uses: haya14busa/action-cond@v1 108 | id: docs_target 109 | with: 110 | cond: ${{ github.repository == 'micronaut-projects/micronaut-core' }} 111 | if_true: "micronaut-projects/micronaut-docs" 112 | if_false: ${{ github.repository }} 113 | 114 | - name: "📑 Publish to Github Pages" 115 | if: success() && github.event_name == 'push' && matrix.java == '21' 116 | uses: micronaut-projects/github-pages-deploy-action@master 117 | env: 118 | TARGET_REPOSITORY: ${{ steps.docs_target.outputs.value }} 119 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 120 | BRANCH: gh-pages 121 | FOLDER: build/docs 122 | 123 | - name: "❓ Optional cleanup step" 124 | run: | 125 | [ -f ./cleanup.sh ] && ./cleanup.sh || [ ! -f ./cleanup.sh ] 126 | -------------------------------------------------------------------------------- /acme/src/main/java/io/micronaut/acme/background/AcmeCertRefresherTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 original authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.micronaut.acme.background; 17 | 18 | import io.micronaut.acme.AcmeConfiguration; 19 | import io.micronaut.acme.services.AcmeService; 20 | import io.micronaut.runtime.event.ApplicationStartupEvent; 21 | import io.micronaut.runtime.event.annotation.EventListener; 22 | import io.micronaut.runtime.exceptions.ApplicationStartupException; 23 | import io.micronaut.scheduling.annotation.Scheduled; 24 | import jakarta.inject.Singleton; 25 | import org.shredzone.acme4j.exception.AcmeException; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | import java.security.cert.X509Certificate; 30 | import java.time.Instant; 31 | import java.time.temporal.ChronoUnit; 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | 35 | /** 36 | * Background task to automatically refresh the certificates from an ACME server on a configurable interval. 37 | */ 38 | @Singleton 39 | public final class AcmeCertRefresherTask { 40 | 41 | private static final Logger LOG = LoggerFactory.getLogger(AcmeCertRefresherTask.class); 42 | 43 | private AcmeService acmeService; 44 | private final AcmeConfiguration acmeConfiguration; 45 | 46 | /** 47 | * Constructs a new Acme cert refresher background task. 48 | * 49 | * @param acmeService Acme service 50 | * @param acmeConfiguration Acme configuration 51 | */ 52 | public AcmeCertRefresherTask(AcmeService acmeService, AcmeConfiguration acmeConfiguration) { 53 | this.acmeService = acmeService; 54 | this.acmeConfiguration = acmeConfiguration; 55 | } 56 | 57 | /** 58 | * Scheduled task to refresh certs from ACME server. 59 | * 60 | * @throws AcmeException if any issues occur during certificate renewal 61 | */ 62 | @Scheduled( 63 | fixedDelay = "${acme.refresh.frequency:24h}", 64 | initialDelay = "${acme.refresh.delay:24h}") 65 | void backgroundRenewal() throws AcmeException { 66 | if (LOG.isDebugEnabled()) { 67 | LOG.debug("Running background/scheduled renewal process"); 68 | } 69 | renewCertIfNeeded(); 70 | } 71 | 72 | /** 73 | * Checks to see if certificate needs renewed on app startup. 74 | * 75 | * @param startupEvent Startup event 76 | */ 77 | @EventListener 78 | void onStartup(ApplicationStartupEvent startupEvent) { 79 | try { 80 | if (LOG.isDebugEnabled()) { 81 | LOG.debug("Running startup renewal process"); 82 | } 83 | renewCertIfNeeded(); 84 | } catch (Exception e) { //NOSONAR 85 | LOG.error("Failed to initialize certificate for SSL no requests would be secure. Stopping application", e); 86 | throw new ApplicationStartupException("Failed to start due to SSL configuration issue.", e); 87 | } 88 | } 89 | 90 | /** 91 | * Does the work to actually renew the certificate if it needs to be done. 92 | * @throws AcmeException if any issues occur during certificate renewal 93 | */ 94 | protected void renewCertIfNeeded() throws AcmeException { 95 | if (!acmeConfiguration.isTosAgree()) { 96 | throw new IllegalStateException(String.format("Cannot refresh certificates until terms of service is accepted. Please review the TOS for Let's Encrypt and set \"%s\" to \"%s\" in configuration once complete", "acme.tos-agree", "true")); 97 | } 98 | 99 | List domains = new ArrayList<>(); 100 | for (String domain : acmeConfiguration.getDomains()) { 101 | domains.add(domain); 102 | if (domain.startsWith("*.")) { 103 | String baseDomain = domain.substring(2); 104 | if (LOG.isDebugEnabled()) { 105 | LOG.debug("Configured domain is a wildcard, including the base domain [{}] in addition", baseDomain); 106 | } 107 | domains.add(baseDomain); 108 | } 109 | } 110 | 111 | X509Certificate currentCertificate = acmeService.getCurrentCertificate(); 112 | if (currentCertificate != null) { 113 | long daysTillExpiration = ChronoUnit.SECONDS.between(Instant.now(), currentCertificate.getNotAfter().toInstant()); 114 | 115 | if (daysTillExpiration <= acmeConfiguration.getRenewWitin().getSeconds()) { 116 | acmeService.orderCertificate(domains); 117 | } else { 118 | acmeService.setupCurrentCertificate(); 119 | } 120 | } else { 121 | acmeService.orderCertificate(domains); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/challenges/AcmeCertRefresherTaskDns01ChallengeSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme.challenges 2 | 3 | import io.micronaut.acme.AcmeBaseSpec 4 | import io.micronaut.acme.challenge.dns.DnsChallengeSolver 5 | import io.micronaut.context.annotation.Replaces 6 | import io.micronaut.http.HttpRequest 7 | import io.micronaut.http.HttpResponse 8 | import io.micronaut.http.annotation.Controller 9 | import io.micronaut.http.annotation.Get 10 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 11 | import jakarta.inject.Inject 12 | import jakarta.inject.Singleton 13 | import org.slf4j.Logger 14 | import org.slf4j.LoggerFactory 15 | import spock.lang.Stepwise 16 | import spock.util.concurrent.PollingConditions 17 | 18 | import javax.net.ssl.HttpsURLConnection 19 | import javax.net.ssl.SSLContext 20 | import java.security.SecureRandom 21 | import java.security.cert.Certificate 22 | import java.security.cert.X509Certificate 23 | 24 | @Stepwise 25 | class AcmeCertRefresherTaskDns01ChallengeSpec extends AcmeBaseSpec { 26 | Map getConfiguration(){ 27 | super.getConfiguration() << [ 28 | "acme.domains": EXPECTED_ACME_DOMAIN, 29 | "acme.challenge-type" : "dns", 30 | "micronaut.server.dualProtocol": true, 31 | "micronaut.server.port" : expectedHttpPort 32 | ] 33 | } 34 | 35 | @Override 36 | Map getPebbleEnv(){ 37 | return [ 38 | "PEBBLE_VA_ALWAYS_VALID": "1" 39 | ] 40 | } 41 | 42 | TestDnsChallengeSolver getTestDnsChallengeSolver() { 43 | embeddedServer.applicationContext.findBean(TestDnsChallengeSolver).get() 44 | } 45 | 46 | def "get new certificate using existing account"() { 47 | expect: 48 | new PollingConditions(timeout: 30).eventually { 49 | certFolder.list().length == 2 50 | certFolder.list().contains("domain.crt") 51 | certFolder.list().contains("domain.csr") 52 | } 53 | } 54 | 55 | def "expect record to be created and match domain"() { 56 | expect: 57 | getTestDnsChallengeSolver().getCreatedRecords().size() == 1 58 | getTestDnsChallengeSolver().getCreatedRecords().containsKey(EXPECTED_ACME_DOMAIN) 59 | getTestDnsChallengeSolver().getCreatedRecords()[EXPECTED_ACME_DOMAIN].length() > 1 60 | } 61 | 62 | def "expect record to be destroyed and match domain"() { 63 | expect: 64 | getTestDnsChallengeSolver().getPurgedRecords() == [EXPECTED_ACME_DOMAIN] 65 | } 66 | 67 | void "expect the url to be https"() { 68 | expect: 69 | embeddedServer.getURL().toString() == "https://$EXPECTED_DOMAIN:$expectedSecurePort" 70 | } 71 | 72 | void "test certificate is one from pebble server"() { 73 | given: "we allow java to trust all certs since the test certs are not 100% valid" 74 | SSLContext sc = SSLContext.getInstance("SSL") 75 | sc.init(null, InsecureTrustManagerFactory.INSTANCE.trustManagers, new SecureRandom()) 76 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()) 77 | 78 | when: "we get the cert that has been setup" 79 | URL destinationURL = new URL(embeddedServer.getURL().toString() + "/dnschallenge") 80 | HttpsURLConnection conn = (HttpsURLConnection) destinationURL.openConnection() 81 | conn.connect() 82 | Certificate[] certs = conn.getServerCertificates() 83 | 84 | then: "we make sure they are from the pebble test server and the domain is as expected" 85 | certs.length > 0 86 | def cert = (X509Certificate) certs[0] 87 | cert.getIssuerX500Principal().getName().contains("Pebble Intermediate CA") 88 | cert.getSubjectX500Principal().getName().contains(EXPECTED_ACME_DOMAIN) 89 | cert.getSubjectAlternativeNames().size() == 1 90 | } 91 | 92 | void "test send https request when the cert is in place"() { 93 | when: 94 | HttpResponse response = client.toBlocking().exchange( 95 | HttpRequest.GET("/dnschallenge"), String 96 | ) 97 | 98 | then: 99 | response.body() == "Hello DNS" 100 | } 101 | 102 | @Controller('/') 103 | static class SslController { 104 | 105 | @Get('/dnschallenge') 106 | String simple() { 107 | return "Hello DNS" 108 | } 109 | 110 | } 111 | 112 | @Singleton 113 | @Replaces(DnsChallengeSolver.class) 114 | static class TestDnsChallengeSolver implements DnsChallengeSolver { 115 | Map createdRecords = [:] 116 | List purgedRecords = [] 117 | static Logger LOG = LoggerFactory.getLogger(TestDnsChallengeSolver.class) 118 | 119 | Map getCreatedRecords() { 120 | createdRecords 121 | } 122 | 123 | List getPurgedRecords() { 124 | purgedRecords 125 | } 126 | 127 | @Override 128 | void createRecord(String domain, String digest) { 129 | LOG.debug("Creating TXT record for {} with value of {}, before data = {}", domain, digest, createdRecords) 130 | createdRecords.put(domain, digest) 131 | LOG.debug("TXT record created, data = {}", createdRecords) 132 | } 133 | 134 | @Override 135 | void destroyRecord(String domain) { 136 | LOG.debug("Destroying TXT record for {}, before data = {}", domain, purgedRecords) 137 | purgedRecords.add(domain) 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/AcmeCertRefresherTaskSetsTimeoutSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme 2 | 3 | import io.micronaut.context.ApplicationContext 4 | import io.micronaut.core.io.socket.SocketUtils 5 | import io.micronaut.mock.slow.SlowAcmeServer 6 | import io.micronaut.mock.slow.SlowServerConfig 7 | import io.micronaut.runtime.exceptions.ApplicationStartupException 8 | import io.micronaut.runtime.server.EmbeddedServer 9 | import org.shredzone.acme4j.exception.AcmeNetworkException 10 | import org.shredzone.acme4j.util.KeyPairUtils 11 | import org.testcontainers.shaded.org.apache.commons.lang3.exception.ExceptionUtils 12 | import spock.lang.AutoCleanup 13 | import spock.lang.Shared 14 | import spock.lang.Specification 15 | 16 | import java.net.http.HttpTimeoutException 17 | import java.security.KeyPair 18 | import java.time.Duration 19 | 20 | class AcmeCertRefresherTaskSetsTimeoutSpec extends Specification { 21 | 22 | public static final String EXPECTED_DOMAIN = "localhost" 23 | 24 | @Shared 25 | @AutoCleanup("deleteDir") 26 | File certFolder 27 | 28 | @Shared 29 | String accountKey 30 | 31 | @Shared 32 | String domainKey 33 | 34 | @Shared 35 | String acmeServerUrl 36 | 37 | @Shared 38 | int expectedHttpPort 39 | 40 | @Shared 41 | int expectedSecurePort 42 | 43 | @Shared 44 | int expectedAcmePort 45 | 46 | @Shared 47 | int networkTimeoutInSecs 48 | 49 | def setupSpec() { 50 | networkTimeoutInSecs = 2 51 | 52 | generateDomainKeypair() 53 | generateAccountKeypair() 54 | } 55 | 56 | KeyPair generateDomainKeypair() { 57 | // Create a new keys to use for the domain 58 | KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048) 59 | StringWriter domainKeyWriter = new StringWriter() 60 | KeyPairUtils.writeKeyPair(domainKeyPair, domainKeyWriter) 61 | domainKey = domainKeyWriter.toString() 62 | domainKeyPair 63 | } 64 | 65 | KeyPair generateAccountKeypair() { 66 | // Create a new keys to register the account with 67 | KeyPair keyPair = KeyPairUtils.createKeyPair(2048) 68 | StringWriter accountKeyWriter = new StringWriter() 69 | KeyPairUtils.writeKeyPair(keyPair, accountKeyWriter) 70 | accountKey = accountKeyWriter.toString() 71 | keyPair 72 | } 73 | 74 | Map getConfiguration() { 75 | certFolder = File.createTempDir() 76 | [ 77 | "acme.domains" : EXPECTED_DOMAIN, 78 | "micronaut.server.ssl.enabled" : true, 79 | "micronaut.server.port" : expectedHttpPort, 80 | "micronaut.server.dualProtocol": true, 81 | "micronaut.server.ssl.port" : expectedSecurePort, 82 | "micronaut.server.host" : EXPECTED_DOMAIN, 83 | "acme.tosAgree" : true, 84 | "acme.cert-location" : certFolder.toString(), 85 | "acme.domain-key" : domainKey, 86 | "acme.account-key" : accountKey, 87 | 'acme.acme-server' : acmeServerUrl, 88 | 'acme.enabled' : true, 89 | ] as Map 90 | } 91 | 92 | def "validate timeout applied if signup is #config"(SlowServerConfig config) { 93 | given: "we have all the ports we could ever need" 94 | expectedHttpPort = SocketUtils.findAvailableTcpPort() 95 | expectedSecurePort = SocketUtils.findAvailableTcpPort() 96 | expectedAcmePort = SocketUtils.findAvailableTcpPort() 97 | acmeServerUrl = "http://localhost:$expectedAcmePort/acme/dir" 98 | 99 | and: "we have a slow acme server" 100 | EmbeddedServer mockAcmeServer = ApplicationContext.builder(['micronaut.server.port': expectedAcmePort]) 101 | .environments("test") 102 | .packages(SlowAcmeServer.getPackage().getName(), AcmeCertRefresherTaskSetsTimeoutSpec.getPackage().getName()) 103 | .run(EmbeddedServer) 104 | SlowAcmeServer slowAcmeServer = mockAcmeServer.getApplicationContext().getBean(SlowAcmeServer.class) 105 | slowAcmeServer.setAcmeServerUrl(acmeServerUrl) 106 | slowAcmeServer.setSlowServerConfig(config) 107 | 108 | 109 | when: "we configure network timeouts" 110 | EmbeddedServer appServer = ApplicationContext.run(EmbeddedServer, 111 | getConfiguration() << ["acme.timeout": "${networkTimeoutInSecs}s"], 112 | "test") 113 | 114 | then: "we get network errors b/c of the timeout" 115 | ApplicationStartupException ex = thrown() 116 | 117 | def ane = ExceptionUtils.getThrowables(ex).find { it instanceof AcmeNetworkException } 118 | ane?.message == "Network error" 119 | 120 | Throwable rootEx = ExceptionUtils.getRootCause(ex) 121 | rootEx instanceof HttpTimeoutException 122 | rootEx.message == "request timed out" 123 | 124 | cleanup: 125 | appServer?.stop() 126 | mockAcmeServer?.stop() 127 | 128 | where: 129 | config | _ 130 | new ActualSlowServerConfig(slowSignup: true) | _ 131 | new ActualSlowServerConfig(slowOrdering: true) | _ 132 | new ActualSlowServerConfig(slowAuthorization: true) | _ 133 | } 134 | 135 | class ActualSlowServerConfig implements SlowServerConfig { 136 | 137 | boolean slowSignup 138 | boolean slowOrdering 139 | boolean slowAuthorization 140 | Duration duration = Duration.ofSeconds(networkTimeoutInSecs + 2) 141 | 142 | String toString() { 143 | "slowSignup: $slowSignup, slowOrdering: $slowOrdering, slowAuthorization: $slowAuthorization, duration: $duration" 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/events/CertificateEventSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme.events 2 | 3 | 4 | import org.shredzone.acme4j.util.KeyPairUtils 5 | import spock.lang.Specification 6 | 7 | import java.security.KeyPair 8 | import java.security.cert.CertificateFactory 9 | import java.security.cert.X509Certificate 10 | 11 | class CertificateEventSpec extends Specification { 12 | static final String X509_CERT = "X.509" 13 | 14 | def DOMAIN_CERT = """ 15 | -----BEGIN CERTIFICATE----- 16 | MIIDUDCCAjigAwIBAgIIHuspA0mthF8wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE 17 | AxMVUGViYmxlIFJvb3QgQ0EgMTU5ZjdmMCAXDTIxMDcxODIxMDczNloYDzIwNTEw 18 | NzE4MjEwNzM2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDY2 19 | NjViYjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALnw4xa4KQCxbhiW 20 | 1o1VYVUbof2mWwkhsWRuws4Uc1kvAVM7k1RZvwNxGQLgJkdXjbUrctddHdFMtksN 21 | imNy/nmB6LKoulzwDL1omCdaiYOxJr93cGYQC3FTm/RaTpaHuec+BaB2Y1iOzbBj 22 | sLL9121eRWUZ0vjaqKwNO8NUlK/geELNgoteIJ1MjOzWp1bryjnaszBfg0eiidD8 23 | 4gV36fvrM1UVJZJ4LBV4QHrKVXl7JA5hn9uk7zucH/XEG87DO2DCWJIwZK9Fm8wD 24 | qMmvx/QH+dwXOXe6kXTDuyu7jJMoHDBLNQ9o4gjkqqHxA1f0ewgo64ObJ09hx+96 25 | fuC49x8CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB 26 | BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFA56gdVb 27 | +6pOjZzcKcjPhe7fWr5WMB8GA1UdIwQYMBaAFD8kVZzZs7SeJSiQFrf5AsH/EFmv 28 | MA0GCSqGSIb3DQEBCwUAA4IBAQApcSZ5s0VGT1KgsXh3GrqxwlSyFfVuE4qvMabf 29 | rXAhUbG3C6hgdA2AWA5IUvI9fRqul6m88hLZc8hrgOJ0vGDAD2u/PMdrqtAz8fV4 30 | gch5z+Jn4J+9Af7hOm3DSFtVRqvbtyWTT2ht7wJbtxAOsuD7+Wa6lr+lZxhHXbRv 31 | RpY6uVNZNlnC5k8BFnx8S9SdsK+upYtkgyKLoFpDhyXgmFMJPGA7UY6NQQ1sA/2x 32 | dwYXMfCY829k0hcxcXYC4SYDjwHxF6YIM4lYS8pT0Z8d98H5cK7WNwFmW+izu6cx 33 | 87DDk/ZlkyArnozVQ6GFJClfhbKZfPKty1r1Y1psSOAUcUD1 34 | -----END CERTIFICATE----- 35 | """ 36 | 37 | def ROOT_CERTIFICATE = """ 38 | -----BEGIN CERTIFICATE----- 39 | MIIDezCCAmOgAwIBAgIIBzCDqTIFEj8wDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE 40 | AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA2NjY1YmIwHhcNMjEwNzE4MjEwNzQx 41 | WhcNMjYwNzE4MjEwNzQxWjAnMSUwIwYDVQQDExxob3N0LnRlc3Rjb250YWluZXJz 42 | LmludGVybmFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiwwiBiCF 43 | q1oMiXSOvEjCKkSR5lGu9CDW9UFQgN/UhVG2RyuDojImUOQjOjHe/DWn7g1XKovT 44 | 3it/M1onAnmksvqFd6YwSUKT8epL1K0dyVzgwaPAgjpJZgt/IZvA9ATWILuMJDGB 45 | jdRRUQ+xex3AVbwa5UJYPlK2t1yqL5YPP9WpZ8H3c1F6M2by5VbwIi78LSxPc47m 46 | H35efxWX2DalsDYirgP3bL0/X/yeVw058Iga+9MsF5MELDMuh9fe5N81TcrtKHvW 47 | W4DfBPUFSnA/52G/nltZdgXxyMgErgwHx86dQphZMAGAD+wCXnzewAI9ZWN4iU27 48 | IiP1kQqVP33AoQIDAQABo4GpMIGmMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAU 49 | BggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFXuU 50 | GdoN4vOZ3IiyvXkQw2Dd92cwHwYDVR0jBBgwFoAUDnqB1Vv7qk6NnNwpyM+F7t9a 51 | vlYwJwYDVR0RBCAwHoIcaG9zdC50ZXN0Y29udGFpbmVycy5pbnRlcm5hbDANBgkq 52 | hkiG9w0BAQsFAAOCAQEAEJNY7olfzudkko1FcGq5bCauwB9240uu67YUIJG7y54G 53 | tq2XWYWQ19FAqgb/7iWKq5X2hjp3Ut3x76SCwOKy5Q0dArcxwQYVgMMj9znxH6LL 54 | QBJOPgQDvnxysEXEu4zvR/GV6ZS5ndFKJAPJxklZkdGhhqp15gUnP/1qTGPLEC5j 55 | 7gR/TfCwWsiMvkBmkYyacDvIHPd8QHtISNhL5Y+dww8DeL+F4ALC1dFLdAaT/bdx 56 | RVv802SMY7YAh8FAsnTsKLYNbSk6ZHVbJuBcVbHqGuWueZ43hwmOTF6pIDaIoBg1 57 | zSti1w9hjz913WF0dTg7RWFLU8e3Jo1O9MCnORtcgg== 58 | -----END CERTIFICATE-----""" 59 | 60 | def FULL_CHAIN_CERT = """ 61 | ${ROOT_CERTIFICATE} 62 | ${DOMAIN_CERT} 63 | """ 64 | 65 | def "can get domain keypair"(){ 66 | given: 67 | CertificateFactory cf = CertificateFactory.getInstance(X509_CERT) 68 | X509Certificate cert = cf.generateCertificate(new ByteArrayInputStream(FULL_CHAIN_CERT.bytes)) 69 | KeyPair keyPair = KeyPairUtils.createKeyPair(2048) 70 | 71 | when : 72 | CertificateEvent event = new CertificateEvent(keyPair, new Random().nextBoolean(), cert) 73 | 74 | then: 75 | event.getDomainKeyPair() == keyPair 76 | } 77 | 78 | def "can determine if the event is a validation certificate or not"(){ 79 | given: 80 | CertificateFactory cf = CertificateFactory.getInstance(X509_CERT) 81 | X509Certificate cert = cf.generateCertificate(new ByteArrayInputStream(FULL_CHAIN_CERT.bytes)) 82 | KeyPair keyPair = KeyPairUtils.createKeyPair(2048) 83 | boolean validationCert = new Random().nextBoolean() 84 | 85 | when : 86 | CertificateEvent event = new CertificateEvent(keyPair, validationCert, cert) 87 | 88 | then: 89 | event.isValidationCert() == validationCert 90 | } 91 | 92 | def "when pass single cert the full chain only contains that cert"(){ 93 | given: 94 | CertificateFactory cf = CertificateFactory.getInstance(X509_CERT) 95 | X509Certificate domainCert = cf.generateCertificate(new ByteArrayInputStream(FULL_CHAIN_CERT.bytes)) 96 | KeyPair keyPair = KeyPairUtils.createKeyPair(2048) 97 | 98 | when : 99 | CertificateEvent event = new CertificateEvent(keyPair, new Random().nextBoolean(), domainCert) 100 | 101 | then: 102 | event.getCert() == domainCert 103 | event.getFullCertificateChain().length == 1 104 | event.getFullCertificateChain()[0] == domainCert 105 | } 106 | 107 | def "when full certificate chain passed we can still get the domain specific cert"(){ 108 | given: 109 | CertificateFactory cf = CertificateFactory.getInstance(X509_CERT) 110 | X509Certificate domainCert = cf.generateCertificate(new ByteArrayInputStream(FULL_CHAIN_CERT.bytes)) 111 | Collection certs = cf.generateCertificates(new ByteArrayInputStream(FULL_CHAIN_CERT.bytes)) 112 | KeyPair keyPair = KeyPairUtils.createKeyPair(2048) 113 | boolean expectedValidationCert = new Random().nextBoolean() 114 | 115 | when : 116 | CertificateEvent event = new CertificateEvent(keyPair, expectedValidationCert, certs as X509Certificate[]) 117 | 118 | then: 119 | event.getCert() == domainCert 120 | event.isValidationCert() == expectedValidationCert 121 | event.getFullCertificateChain().length == 2 122 | event.getFullCertificateChain() == certs.toArray() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/acme/AcmeBaseSpec.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.acme 2 | 3 | import io.micronaut.context.ApplicationContext 4 | import io.micronaut.core.io.socket.SocketUtils 5 | import io.micronaut.http.client.HttpClient 6 | import io.micronaut.runtime.server.EmbeddedServer 7 | import org.shredzone.acme4j.Account 8 | import org.shredzone.acme4j.AccountBuilder 9 | import org.shredzone.acme4j.Session 10 | import org.shredzone.acme4j.Status 11 | import org.shredzone.acme4j.util.KeyPairUtils 12 | import org.slf4j.Logger 13 | import org.slf4j.LoggerFactory 14 | import org.testcontainers.Testcontainers 15 | import org.testcontainers.containers.GenericContainer 16 | import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy 17 | import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy 18 | import org.testcontainers.containers.wait.strategy.WaitAllStrategy 19 | import org.testcontainers.utility.MountableFile 20 | import spock.lang.AutoCleanup 21 | import spock.lang.Shared 22 | import spock.lang.Specification 23 | 24 | import java.security.KeyPair 25 | import java.time.Duration 26 | 27 | abstract class AcmeBaseSpec extends Specification { 28 | private static final Logger log = LoggerFactory.getLogger(AcmeBaseSpec.class) 29 | 30 | // Must be this since the docker container can only call the host if its set to this value. See here https://www.testcontainers.org/features/networking#exposing-host-ports-to-the-container 31 | public static final String EXPECTED_ACME_DOMAIN = "host.testcontainers.internal" 32 | public static final String EXPECTED_DOMAIN = "localhost" 33 | public static final int EXPECTED_PORT = 8443 34 | @Shared 35 | GenericContainer certServerContainer 36 | 37 | @Shared 38 | @AutoCleanup 39 | EmbeddedServer embeddedServer 40 | 41 | @Shared 42 | @AutoCleanup 43 | HttpClient client 44 | 45 | @Shared 46 | @AutoCleanup("deleteDir") 47 | File certFolder 48 | 49 | @Shared 50 | String accountKey 51 | 52 | @Shared 53 | String domainKey 54 | 55 | @Shared 56 | String acmeServerUrl 57 | 58 | @Shared 59 | int expectedHttpPort 60 | 61 | @Shared 62 | int expectedSecurePort 63 | 64 | @Shared 65 | int expectedPebbleServerPort 66 | 67 | def setupSpec() { 68 | expectedHttpPort = SocketUtils.findAvailableTcpPort() 69 | expectedSecurePort = SocketUtils.findAvailableTcpPort() 70 | expectedPebbleServerPort = SocketUtils.findAvailableTcpPort() 71 | 72 | certServerContainer = startPebbleContainer(expectedHttpPort, expectedSecurePort, expectedPebbleServerPort, getPebbleEnv()) 73 | 74 | KeyPair keyPair = getAccountKeypair() 75 | getDomainKeypair() 76 | 77 | acmeServerUrl = "acme://pebble/${certServerContainer.containerIpAddress}:${certServerContainer.getMappedPort(expectedPebbleServerPort)}" 78 | 79 | // Create an account with the acme server 80 | Session session = new Session(acmeServerUrl) 81 | Account createNewAccount = new AccountBuilder() 82 | .agreeToTermsOfService() 83 | .addEmail("test@micronaut.io") 84 | .useKeyPair(keyPair) 85 | .create(session) 86 | assert createNewAccount.status == Status.VALID 87 | 88 | embeddedServer = ApplicationContext.run(EmbeddedServer, 89 | getConfiguration(), 90 | "test") 91 | 92 | client = embeddedServer.getApplicationContext().createBean(HttpClient, embeddedServer.getURL()) 93 | } 94 | 95 | static GenericContainer startPebbleContainer(int expectedHttpPort, int expectedSecurePort, int expectedPebbleServerPort, Map pebbleEnvConfig) { 96 | Testcontainers.exposeHostPorts(expectedHttpPort, expectedSecurePort) 97 | 98 | def file = File.createTempFile("pebble", "config") 99 | file.write """{ 100 | "pebble": { 101 | "listenAddress": "0.0.0.0:${expectedPebbleServerPort}", 102 | "certificate": "test/certs/localhost/cert.pem", 103 | "privateKey": "test/certs/localhost/key.pem", 104 | "httpPort": $expectedHttpPort, 105 | "tlsPort": $expectedSecurePort 106 | } 107 | }""" 108 | 109 | log.info("Expected micronaut ports - http : {}, secure : {} ", expectedHttpPort, expectedSecurePort) 110 | log.info("Expected pebble config : {}", file.text) 111 | 112 | GenericContainer certServerContainer = new GenericContainer("letsencrypt/pebble:latest") 113 | .withCopyFileToContainer(MountableFile.forHostPath(file.toPath()), "/test/config/pebble-config.json") 114 | .withCommand("/usr/bin/pebble", "-strict", "false") 115 | .withEnv(pebbleEnvConfig) 116 | .withExposedPorts(expectedPebbleServerPort) 117 | .waitingFor(new WaitAllStrategy().withStrategy(new LogMessageWaitStrategy().withRegEx(".*ACME directory available.*\n")) 118 | .withStrategy(new HostPortWaitStrategy()) 119 | .withStartupTimeout(Duration.ofMinutes(2))); 120 | certServerContainer.start() 121 | return certServerContainer 122 | } 123 | 124 | KeyPair getDomainKeypair() { 125 | // Create a new keys to use for the domain 126 | KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048) 127 | StringWriter domainKeyWriter = new StringWriter() 128 | KeyPairUtils.writeKeyPair(domainKeyPair, domainKeyWriter) 129 | domainKey = domainKeyWriter.toString() 130 | domainKeyPair 131 | } 132 | 133 | KeyPair getAccountKeypair() { 134 | // Create a new keys to register the account with 135 | KeyPair keyPair = KeyPairUtils.createKeyPair(2048) 136 | StringWriter accountKeyWriter = new StringWriter() 137 | KeyPairUtils.writeKeyPair(keyPair, accountKeyWriter) 138 | accountKey = accountKeyWriter.toString() 139 | keyPair 140 | } 141 | 142 | Map getPebbleEnv() { 143 | return [ 144 | "PEBBLE_VA_ALWAYS_VALID": "1" 145 | ] 146 | } 147 | 148 | Map getConfiguration() { 149 | certFolder = File.createTempDir() 150 | [ 151 | "micronaut.server.ssl.enabled": true, 152 | "micronaut.server.ssl.port" : expectedSecurePort, 153 | "micronaut.server.host" : EXPECTED_DOMAIN, 154 | "micronaut.http.client.ssl.insecure-trust-all-certificates": true, 155 | "acme.tosAgree" : true, 156 | "acme.cert-location" : certFolder.toString(), 157 | "acme.domain-key" : domainKey, 158 | "acme.account-key" : accountKey, 159 | 'acme.acme-server' : acmeServerUrl, 160 | 'acme.enabled' : true, 161 | 'acme.order.pause' : "1s", 162 | 'acme.auth.pause' : "1s" 163 | ] as Map 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /acme/src/test/groovy/io/micronaut/mock/slow/SlowAcmeServer.groovy: -------------------------------------------------------------------------------- 1 | package io.micronaut.mock.slow 2 | 3 | import io.micronaut.context.annotation.Requires 4 | import io.micronaut.http.HttpResponse 5 | import io.micronaut.http.annotation.Consumes 6 | import io.micronaut.http.annotation.Controller 7 | import io.micronaut.http.annotation.Get 8 | import io.micronaut.http.annotation.Head 9 | import io.micronaut.http.annotation.Post 10 | import io.netty.handler.ssl.util.SelfSignedCertificate 11 | 12 | import java.time.Duration 13 | import java.util.concurrent.atomic.AtomicInteger 14 | 15 | @Requires(env = "test") 16 | @Controller('/acme') 17 | class SlowAcmeServer { 18 | 19 | String expires = new Date().plus(30).format("yyyy-MM-dd'T'HH:mm'Z'") 20 | String domain = "yourdomain.com" 21 | String acmeServerUrl 22 | 23 | SlowServerConfig slowServerConfig 24 | 25 | void setSlowServerConfig(SlowServerConfig config) { 26 | this.slowServerConfig = config 27 | } 28 | 29 | void setAcmeServerUrl(String acmeServerUrl) { 30 | this.acmeServerUrl = acmeServerUrl.replaceAll("/dir", "") 31 | } 32 | 33 | @Get('/dir') 34 | String dirGet() { 35 | return getDirListing() 36 | } 37 | 38 | @Post('/dir') 39 | String dirPost() { 40 | return getDirListing() 41 | } 42 | 43 | AtomicInteger requestCounter = new AtomicInteger() 44 | 45 | @Consumes("application/jose+json") 46 | @Post('/your-order') 47 | String yourOrder() { 48 | if (slowServerConfig.isSlowOrdering()) { 49 | doItSlowly(slowServerConfig.duration) 50 | } 51 | int requestCount = requestCounter.getAndIncrement() 52 | if (requestCount % 2) { 53 | """ 54 | { 55 | "status":"valid", 56 | "expires":"$expires", 57 | "identifiers":[ 58 | { 59 | "type":"dns", 60 | "value":"$domain" 61 | } 62 | ], 63 | "authorizations":[ 64 | "$acmeServerUrl/authz" 65 | ], 66 | "finalize":"$acmeServerUrl/finalize", 67 | "certificate":"$acmeServerUrl/cert" 68 | } 69 | """ 70 | } else { 71 | """ 72 | { 73 | "status":"ready", 74 | "expires": "$expires", 75 | "identifiers":[ 76 | { 77 | "type":"dns", 78 | "value":"doit.is-it-friday.org" 79 | } 80 | ], 81 | "authorizations":[ 82 | "$acmeServerUrl/authz" 83 | ], 84 | "finalize":"$acmeServerUrl/finalize" 85 | } 86 | """ 87 | } 88 | } 89 | 90 | @Consumes("application/jose+json") 91 | @Post('cert') 92 | HttpResponse cert() { 93 | HttpResponse.ok(new SelfSignedCertificate(domain).certificate().readBytes()) 94 | .header("Content-Type", "application/pem-certificate-chain") 95 | } 96 | 97 | @Consumes("application/jose+json") 98 | @Post('/sign-me-up') 99 | HttpResponse signmeup() { 100 | if (slowServerConfig.isSlowSignup()) { 101 | doItSlowly(slowServerConfig.duration) 102 | } 103 | return HttpResponse.ok( 104 | """{ 105 | "key":{ 106 | "kty":"RSA", 107 | "n":"xxxtRGLtg0Eqtb_ZfwLegsld46EGp7MHRtK8z1kD5zto8kWozm5s_9NQ-Htlakd94pZOmpCBg6G8i8Izc3doFqSeY9P7khf0dUIbF7K6SdwmXsAYEkCE0XmSrRBCzft82yW2jNBRsaFl-gRZkJu82L4Zleee", 108 | "e":"ZZAB" 109 | }, 110 | "contact":[ 111 | "mailto:testing@testing.com" 112 | ], 113 | "initialIp":"24.456.231.199", 114 | "createdAt":"2017-05-24T01:32:46Z", 115 | "status":"valid" 116 | }""") 117 | .header("Location", "$acmeServerUrl/your-account") 118 | } 119 | 120 | @Head('/nonce-plz') 121 | HttpResponse nonce() { 122 | return HttpResponse.ok("nonce") 123 | .header("Replay-Nonce", "nonce") 124 | } 125 | 126 | @Consumes("application/jose+json") 127 | @Post('/finalize') 128 | String finalizeOrder() { 129 | if (slowServerConfig.isSlowOrdering()) { 130 | doItSlowly(slowServerConfig.duration) 131 | } 132 | "profit" 133 | } 134 | 135 | @Consumes("application/jose+json") 136 | @Post('/order-plz') 137 | HttpResponse order() { 138 | if (slowServerConfig.isSlowOrdering()) { 139 | doItSlowly(slowServerConfig.duration) 140 | } 141 | return HttpResponse.ok( 142 | """ 143 | { 144 | "status":"ready", 145 | "expires": "$expires", 146 | "identifiers":[ 147 | { 148 | "type":"dns", 149 | "value": "$domain" 150 | } 151 | ], 152 | "authorizations":[ 153 | "$acmeServerUrl/authz" 154 | ], 155 | "finalize":"$acmeServerUrl/finalize" 156 | } 157 | """) 158 | .header("Location", "$acmeServerUrl/your-order") 159 | } 160 | 161 | @Consumes("application/jose+json") 162 | @Post('/authz') 163 | String authz() { 164 | if (slowServerConfig.isSlowAuthorization()) { 165 | doItSlowly(slowServerConfig.duration) 166 | } 167 | return """ 168 | { 169 | "identifier":{ 170 | "type":"dns", 171 | "value": "$domain" 172 | }, 173 | "status":"valid", 174 | "expires": "$expires", 175 | "challenges":[ 176 | { 177 | "type":"tls-alpn-01", 178 | "status":"valid", 179 | "url":"$acmeServerUrl/challenge", 180 | "token":"pzzz_Eeee-MxxxG6Ma3-hBBBBOHh-5oBEEFtE", 181 | "validationRecord":[ 182 | { 183 | "hostname": "$domain", 184 | "port":"443", 185 | "addressesResolved":[ 186 | "127.0.0.1" 187 | ], 188 | "addressUsed":"127.0.0.1" 189 | } 190 | ] 191 | } 192 | ] 193 | } 194 | """ 195 | } 196 | 197 | private String getDirListing() { 198 | return """ 199 | { 200 | "keyChange": "$acmeServerUrl/rollover-account-key", 201 | "meta": { 202 | "termsOfService": "data:text/plain,Do%20what%20thou%20wilt" 203 | }, 204 | "newAccount": "$acmeServerUrl/sign-me-up", 205 | "newNonce": "$acmeServerUrl/nonce-plz", 206 | "newOrder": "$acmeServerUrl/order-plz", 207 | "revokeCert": "$acmeServerUrl/revoke-cert" 208 | } 209 | """ 210 | } 211 | 212 | private void doItSlowly(Duration sleepTime) { 213 | println ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" 214 | println ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" 215 | println ">>>>>>>>>>>>>> DOING IT SLOWLY >>>>>>>>>>" 216 | println ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" 217 | println ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" 218 | Thread.sleep(sleepTime.toMillis()) 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /.clinerules/coding.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Micronaut Development Guide 3 | author: Cédric Champeau 4 | version: 1.0 5 | globs: ["**/*.java", "**/*.kts", "**/*.xml"] 6 | --- 7 | 8 | # Micronaut Project Development Guide 9 | 10 | ## Project Overview 11 | 12 | This project is a Micronaut module which is part of the Micronaut Framework, a modern, JVM-based, full-stack framework for building modular, easily testable microservice and serverless applications. 13 | 14 | The description of what this project is doing can be found in the `gradle.properties` file under the `projectDesc` key. 15 | 16 | This repository does not represent an actual application; its modules are designed to be used as dependencies by applications. 17 | The root project MUST NOT contain any code: it is a parent project which coordinates the build and supplies documentation. 18 | 19 | ⚠️ CRITICAL: DO NOT USE attempt_completion BEFORE TESTING ⚠️ 20 | 21 | ## Command Conventions 22 | 23 | - You MUST run all Gradle commands (except testing ones) with the quiet option to reduce output: 24 | - Do NOT use: `--stacktrace`, `--info` (excessive output will not fit the context) 25 | - You MUST run all Gradle testing commands without the `-q` option, since that will suppress the output result of each test. 26 | - Gradle project names are prefixed with `micronaut-`. For example, directory `mylib` maps to Gradle project `:micronaut-mylib`. 27 | - You MUST run Gradle with the Gradle wrapper: `./gradlew`. 28 | - Gradle project names prefixed with `test-` or `test-suite` are not intended for users: they are functional tests of the project 29 | 30 | ## Main Development Tasks: 31 | 32 | - Compile (module): `./gradlew -q ::compileTestJava` 33 | - Test (module): `./gradlew ::test` 34 | - Checkstyle (aggregate task): `./gradlew -q cM` 35 | - Note: `cM` is the canonical Checkstyle runner defined in build logic to validate Checkstyle across the codebase. 36 | - Note: you MUST NOT introduce new warnings. You SHOULD fix warnings in code that you have modified. 37 | - Spotless (check license headers/format): `./gradlew -q spotlessCheck` 38 | - Spotless (auto-fix formatting/headers): `./gradlew -q spotlessApply` (MUST be used to fix violations found by `spotlessCheck`) 39 | 40 | ## Code style 41 | 42 | You SHOULD prefer modern Java idioms: records, pattern matching, sealed interfaces/classes, `var` for local variables. 43 | You MUST NOT use fully qualified class names unless there is a conflict between 2 class names in different packages. 44 | You MUST annotate the code with nullability annotations (`org.jspecify.annotations.Nullable`, `org.jspecify.annotations.NonNull`). 45 | You MUST NOT use reflection: Micronaut is a reflection-free framework tailored for integration with GraalVM. 46 | You MUST use `jakarta.inject` for dependency injection, NOT `javax.inject`. 47 | 48 | ## Binary compatibility 49 | 50 | Micronaut projects are intended to be used in consumer applications and therefore follow semantic versioning. As a consequence: 51 | - You MUST NOT break any public facing API without explicit consent 52 | - You SHOULD run the `./gradlew japiCmp` task to get a report about binary breaking changes 53 | - You SHOULD reduce the visibility of members for non user-facing APIs. 54 | - You MUST annotate non-user facing APIs with `@io.micronaut.core.annotation.Internal` 55 | 56 | ## Implementation Workflow (Required Checklist) 57 | 58 | You MUST follow this sequence after editing source files: 59 | 60 | 1) Compile affected modules 61 | - `./gradlew -q ::compileTestJava ::compileTestGroovy` 62 | 63 | 2) Run targeted tests first (fast feedback) 64 | - `./gradlew ::test --tests 'pkg.ClassTest'` 65 | - `./gradlew ::test --tests 'pkg.ClassTest.method'` (optional) 66 | 67 | 3) Run full tests for all affected modules 68 | - `./gradlew ::test` 69 | 70 | 4) Static checks 71 | - Checkstyle: `./gradlew -q cM` 72 | 73 | 5) (Optional) If, and only if you have created new files, you SHOULD run 74 | - Spotless check: `./gradlew -q spotlessCheck` 75 | - If Spotless fails: `./gradlew -q spotlessApply` then re-run `spotlessCheck` 76 | - You MUST NOT add new license headers on existing files: only focus on files you have added 77 | 78 | 6) Verify a clean working tree 79 | - You SHOULD ensure no unrelated changes are pending before proposing changes. 80 | - Use `git_status` to verify the working tree: 81 | ```xml 82 | 83 | mcp-server-git 84 | git_status 85 | 86 | { 87 | "repo_path": "/home/cchampeau/DEV/PROJECTS/micronaut/micronaut-langchain4j" // adjust absolute path if necessary 88 | } 89 | 90 | 91 | ``` 92 | 93 | ## Documentation Requirements 94 | 95 | - You MUST update documentation when necessary, following the project’s documentation rules in `.clinerules/docs.md`. 96 | - Before writing code, you SHOULD analyze relevant code files to get full context, then implement changes with minimal surface area. 97 | - You SHOULD list assumptions and uncertainties that need clarification before completing a task. 98 | - You SHOULD check project configuration/build files before proposing structural or dependency changes. 99 | 100 | ## Context7 Usage (Documentation and Examples) 101 | 102 | You MUST use Context7 to get up-to-date, version-specific documentation and code examples for frameworks and libraries. 103 | 104 | Preferred library IDs: 105 | - Micronaut main docs: `/websites/micronaut_io` 106 | - Micronaut Test: `/websites/micronaut-projects_github_io_micronaut-test` 107 | - Micronaut Oracle Cloud: `/websites/micronaut-projects_github_io_micronaut-oracle-cloud` 108 | - OpenRewrite: `/openrewrite/rewrite-docs` 109 | 110 | Example (fetch docs for a topic): 111 | ```xml 112 | 113 | context7-mcp 114 | get-library-docs 115 | 116 | { 117 | "context7CompatibleLibraryID": "/openrewrite/rewrite-docs", 118 | "topic": "JavaIsoVisitor" 119 | } 120 | 121 | 122 | ``` 123 | 124 | For other libraries, you MUST resolve the library ID first: 125 | ```xml 126 | 127 | context7-mcp 128 | resolve-library-id 129 | 130 | { 131 | "libraryName": "Mockito" 132 | } 133 | 134 | 135 | ``` 136 | 137 | ## Dependency Management (Version Catalogs) 138 | 139 | - Main dependencies are managed in the Gradle version catalog at `gradle/libs.versions.toml`. 140 | - You MUST use catalogs when adding dependencies (avoid hard-coded coordinates/versions in module builds). 141 | 142 | Adding a new dependency (steps): 143 | 1) Choose or add the version in the appropriate catalog (`libs.versions.toml`). 144 | 2) Add an alias under the relevant section (e.g., `libraries`). 145 | 3) Reference the alias from a module’s `build.gradle.kts`, for example: 146 | - `implementation(libs.some.library)` 147 | - `testImplementation(testlibs.some.junit)` 148 | 4) Do NOT hardcode versions in module build files; use the catalog entries. 149 | 150 | You SHOULD choose the appropriate scope depending on the use of the library: 151 | - `api` for dependencies which appear in public signatures or the API of a module 152 | - `implementation` for dependencies which are implementation details, only used in the method bodies for example 153 | - `compileOnly` for dependencies which are only required at build time but not at runtime 154 | - `runtimeOnly` for dependencies which are only required at run time and not at compile time 155 | 156 | ## Build logic 157 | 158 | Micronaut projects follow Gradle best practices, in particular usage of convention plugins. 159 | Convention plugins live under the `buildSrc` directory. 160 | 161 | You MUST NOT add custom build logic directly in `build.gradle(.kts)` files. 162 | You MUST implement build logic as part of convention plugins. 163 | You SHOULD avoid build logic code duplication by moving common build logic into custom convention plugins. 164 | You SHOULD try to prefer composition of convention plugins. 165 | 166 | ## Key Requirements 167 | 168 | You MUST confirm all of the following BEFORE using `attempt_completion`: 169 | 170 | - Changes compile successfully (affected modules) 171 | - Targeted tests pass 172 | - Full tests for affected modules pass 173 | - Checkstyle (`cM`) passes 174 | - Spotless (`spotlessCheck`) passes (apply fixes if needed) 175 | - Documentation updated when necessary 176 | - Working tree is clean (no unrelated diffs) 177 | 178 | If ANY item is “no”, you MUST NOT use `attempt_completion`. 179 | While you SHOULD add new files using `git add`, you MUST NOT commit (`git commit`) files yourself. 180 | -------------------------------------------------------------------------------- /.github/instructions/coding.instructions.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Micronaut Development Guide 3 | author: Cédric Champeau 4 | version: 1.0 5 | globs: ["**/*.java", "**/*.kts", "**/*.xml"] 6 | --- 7 | 8 | # Micronaut Project Development Guide 9 | 10 | ## Project Overview 11 | 12 | This project is a Micronaut module which is part of the Micronaut Framework, a modern, JVM-based, full-stack framework for building modular, easily testable microservice and serverless applications. 13 | 14 | The description of what this project is doing can be found in the `gradle.properties` file under the `projectDesc` key. 15 | 16 | This repository does not represent an actual application; its modules are designed to be used as dependencies by applications. 17 | The root project MUST NOT contain any code: it is a parent project which coordinates the build and supplies documentation. 18 | 19 | ⚠️ CRITICAL: DO NOT USE attempt_completion BEFORE TESTING ⚠️ 20 | 21 | ## Command Conventions 22 | 23 | - You MUST run all Gradle commands (except testing ones) with the quiet option to reduce output: 24 | - Do NOT use: `--stacktrace`, `--info` (excessive output will not fit the context) 25 | - You MUST run all Gradle testing commands without the `-q` option, since that will suppress the output result of each test. 26 | - Gradle project names are prefixed with `micronaut-`. For example, directory `mylib` maps to Gradle project `:micronaut-mylib`. 27 | - You MUST run Gradle with the Gradle wrapper: `./gradlew`. 28 | - Gradle project names prefixed with `test-` or `test-suite` are not intended for users: they are functional tests of the project 29 | 30 | ## Main Development Tasks: 31 | 32 | - Compile (module): `./gradlew -q ::compileTestJava` 33 | - Test (module): `./gradlew ::test` 34 | - Checkstyle (aggregate task): `./gradlew -q cM` 35 | - Note: `cM` is the canonical Checkstyle runner defined in build logic to validate Checkstyle across the codebase. 36 | - Note: you MUST NOT introduce new warnings. You SHOULD fix warnings in code that you have modified. 37 | - Spotless (check license headers/format): `./gradlew -q spotlessCheck` 38 | - Spotless (auto-fix formatting/headers): `./gradlew -q spotlessApply` (MUST be used to fix violations found by `spotlessCheck`) 39 | 40 | ## Code style 41 | 42 | You SHOULD prefer modern Java idioms: records, pattern matching, sealed interfaces/classes, `var` for local variables. 43 | You MUST NOT use fully qualified class names unless there is a conflict between 2 class names in different packages. 44 | You MUST annotate the code with nullability annotations (`org.jspecify.annotations.Nullable`, `org.jspecify.annotations.NonNull`). 45 | You MUST NOT use reflection: Micronaut is a reflection-free framework tailored for integration with GraalVM. 46 | You MUST use `jakarta.inject` for dependency injection, NOT `javax.inject`. 47 | 48 | ## Binary compatibility 49 | 50 | Micronaut projects are intended to be used in consumer applications and therefore follow semantic versioning. As a consequence: 51 | - You MUST NOT break any public facing API without explicit consent 52 | - You SHOULD run the `./gradlew japiCmp` task to get a report about binary breaking changes 53 | - You SHOULD reduce the visibility of members for non user-facing APIs. 54 | - You MUST annotate non-user facing APIs with `@io.micronaut.core.annotation.Internal` 55 | 56 | ## Implementation Workflow (Required Checklist) 57 | 58 | You MUST follow this sequence after editing source files: 59 | 60 | 1) Compile affected modules 61 | - `./gradlew -q ::compileTestJava ::compileTestGroovy` 62 | 63 | 2) Run targeted tests first (fast feedback) 64 | - `./gradlew ::test --tests 'pkg.ClassTest'` 65 | - `./gradlew ::test --tests 'pkg.ClassTest.method'` (optional) 66 | 67 | 3) Run full tests for all affected modules 68 | - `./gradlew ::test` 69 | 70 | 4) Static checks 71 | - Checkstyle: `./gradlew -q cM` 72 | 73 | 5) (Optional) If, and only if you have created new files, you SHOULD run 74 | - Spotless check: `./gradlew -q spotlessCheck` 75 | - If Spotless fails: `./gradlew -q spotlessApply` then re-run `spotlessCheck` 76 | - You MUST NOT add new license headers on existing files: only focus on files you have added 77 | 78 | 6) Verify a clean working tree 79 | - You SHOULD ensure no unrelated changes are pending before proposing changes. 80 | - Use `git_status` to verify the working tree: 81 | ```xml 82 | 83 | mcp-server-git 84 | git_status 85 | 86 | { 87 | "repo_path": "/home/cchampeau/DEV/PROJECTS/micronaut/micronaut-langchain4j" // adjust absolute path if necessary 88 | } 89 | 90 | 91 | ``` 92 | 93 | ## Documentation Requirements 94 | 95 | - You MUST update documentation when necessary, following the project’s documentation rules in `.clinerules/docs.md`. 96 | - Before writing code, you SHOULD analyze relevant code files to get full context, then implement changes with minimal surface area. 97 | - You SHOULD list assumptions and uncertainties that need clarification before completing a task. 98 | - You SHOULD check project configuration/build files before proposing structural or dependency changes. 99 | 100 | ## Context7 Usage (Documentation and Examples) 101 | 102 | You MUST use Context7 to get up-to-date, version-specific documentation and code examples for frameworks and libraries. 103 | 104 | Preferred library IDs: 105 | - Micronaut main docs: `/websites/micronaut_io` 106 | - Micronaut Test: `/websites/micronaut-projects_github_io_micronaut-test` 107 | - Micronaut Oracle Cloud: `/websites/micronaut-projects_github_io_micronaut-oracle-cloud` 108 | - OpenRewrite: `/openrewrite/rewrite-docs` 109 | 110 | Example (fetch docs for a topic): 111 | ```xml 112 | 113 | context7-mcp 114 | get-library-docs 115 | 116 | { 117 | "context7CompatibleLibraryID": "/openrewrite/rewrite-docs", 118 | "topic": "JavaIsoVisitor" 119 | } 120 | 121 | 122 | ``` 123 | 124 | For other libraries, you MUST resolve the library ID first: 125 | ```xml 126 | 127 | context7-mcp 128 | resolve-library-id 129 | 130 | { 131 | "libraryName": "Mockito" 132 | } 133 | 134 | 135 | ``` 136 | 137 | ## Dependency Management (Version Catalogs) 138 | 139 | - Main dependencies are managed in the Gradle version catalog at `gradle/libs.versions.toml`. 140 | - You MUST use catalogs when adding dependencies (avoid hard-coded coordinates/versions in module builds). 141 | 142 | Adding a new dependency (steps): 143 | 1) Choose or add the version in the appropriate catalog (`libs.versions.toml`). 144 | 2) Add an alias under the relevant section (e.g., `libraries`). 145 | 3) Reference the alias from a module’s `build.gradle.kts`, for example: 146 | - `implementation(libs.some.library)` 147 | - `testImplementation(testlibs.some.junit)` 148 | 4) Do NOT hardcode versions in module build files; use the catalog entries. 149 | 150 | You SHOULD choose the appropriate scope depending on the use of the library: 151 | - `api` for dependencies which appear in public signatures or the API of a module 152 | - `implementation` for dependencies which are implementation details, only used in the method bodies for example 153 | - `compileOnly` for dependencies which are only required at build time but not at runtime 154 | - `runtimeOnly` for dependencies which are only required at run time and not at compile time 155 | 156 | ## Build logic 157 | 158 | Micronaut projects follow Gradle best practices, in particular usage of convention plugins. 159 | Convention plugins live under the `buildSrc` directory. 160 | 161 | You MUST NOT add custom build logic directly in `build.gradle(.kts)` files. 162 | You MUST implement build logic as part of convention plugins. 163 | You SHOULD avoid build logic code duplication by moving common build logic into custom convention plugins. 164 | You SHOULD try to prefer composition of convention plugins. 165 | 166 | ## Key Requirements 167 | 168 | You MUST confirm all of the following BEFORE using `attempt_completion`: 169 | 170 | - Changes compile successfully (affected modules) 171 | - Targeted tests pass 172 | - Full tests for affected modules pass 173 | - Checkstyle (`cM`) passes 174 | - Spotless (`spotlessCheck`) passes (apply fixes if needed) 175 | - Documentation updated when necessary 176 | - Working tree is clean (no unrelated diffs) 177 | 178 | If ANY item is “no”, you MUST NOT use `attempt_completion`. 179 | While you SHOULD add new files using `git add`, you MUST NOT commit (`git commit`) files yourself. 180 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Do not edit this file directly. Instead, go to: 2 | # 3 | # https://github.com/micronaut-projects/micronaut-project-template/tree/master/.github/workflows 4 | # 5 | # and edit them there. Note that it will be sync'ed to all the Micronaut repos 6 | name: Release 7 | on: 8 | release: 9 | types: [published] 10 | jobs: 11 | release: 12 | outputs: 13 | artifacts-sha256: ${{ steps.hash.outputs.artifacts-sha256 }} # Computed hashes for build artifacts. 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Remove system JDKs 17 | run: | 18 | sudo rm -rf /usr/lib/jvm/* 19 | unset JAVA_HOME 20 | export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v '/usr/lib/jvm' | paste -sd:) 21 | - name: Checkout repository 22 | uses: actions/checkout@v6 23 | with: 24 | token: ${{ secrets.GH_TOKEN }} 25 | - uses: gradle/actions/wrapper-validation@v5 26 | - name: Set up JDK 27 | uses: actions/setup-java@v5 28 | with: 29 | distribution: 'temurin' 30 | java-version: | 31 | 21 32 | 25 33 | - name: Set the current release version 34 | id: release_version 35 | run: echo "release_version=${GITHUB_REF:11}" >> $GITHUB_OUTPUT 36 | - name: Run pre-release 37 | uses: micronaut-projects/github-actions/pre-release@master 38 | env: 39 | MICRONAUT_BUILD_EMAIL: ${{ secrets.MICRONAUT_BUILD_EMAIL }} 40 | with: 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | - name: Publish to Sonatype OSSRH 43 | id: publish 44 | env: 45 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 46 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 47 | GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} 48 | GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }} 49 | GPG_FILE: ${{ secrets.GPG_FILE }} 50 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 51 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 52 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 53 | run: | 54 | echo $GPG_FILE | base64 -d > secring.gpg 55 | # Publish both locally and to Sonatype. 56 | # The artifacts stored locally will be used to generate the SLSA provenance. 57 | ./gradlew publishToMavenCentral --publishing-type=AUTOMATIC 58 | # Read the current version from gradle.properties. 59 | VERSION=$(./gradlew properties | grep 'version:' | awk '{print $2}') 60 | # Read the project group from gradle.properties. 61 | GROUP_PATH=$(./gradlew properties| grep "projectGroup" | awk '{print $2}' | sed 's/\./\//g') 62 | echo "version=$VERSION" >> "$GITHUB_OUTPUT" 63 | echo "group=$GROUP_PATH" >> "$GITHUB_OUTPUT" 64 | - name: Generate subject 65 | id: hash 66 | run: | 67 | # Find the artifact JAR and POM files in the local repository. 68 | ARTIFACTS=$(find build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* \ 69 | -type f \( \( -iname "*.jar" -not -iname "*-javadoc.jar" -not -iname "*-sources.jar" \) -or -iname "*.pom" \)) 70 | # Compute the hashes for the artifacts. 71 | # Set the hash as job output for debugging. 72 | echo "artifacts-sha256=$(sha256sum $ARTIFACTS | base64 -w0)" >> "$GITHUB_OUTPUT" 73 | # Store the hash in a file, which is uploaded as a workflow artifact. 74 | sha256sum $ARTIFACTS | base64 -w0 > artifacts-sha256 75 | - name: Upload build artifacts 76 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 77 | with: 78 | name: gradle-build-outputs 79 | path: build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* 80 | retention-days: 5 81 | - name: Upload artifacts-sha256 82 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 83 | with: 84 | name: artifacts-sha256 85 | path: artifacts-sha256 86 | retention-days: 5 87 | - name: Generate docs 88 | run: ./gradlew docs 89 | env: 90 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 91 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 92 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 93 | GH_TOKEN_PUBLIC_REPOS_READONLY: ${{ secrets.GH_TOKEN_PUBLIC_REPOS_READONLY }} 94 | GH_USERNAME: ${{ secrets.GH_USERNAME }} 95 | - name: Export Gradle Properties 96 | uses: micronaut-projects/github-actions/export-gradle-properties@master 97 | - name: LATEST_TAG 98 | run: | 99 | echo "LATEST_TAG=$(curl -s -L -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')" >> $GITHUB_ENV 100 | - name: Publish to Github Pages 101 | if: success() 102 | uses: micronaut-projects/github-pages-deploy-action@master 103 | env: 104 | BETA: ${{ !(github.event.release.tag_name == env.LATEST_TAG) || github.event.release.draft || github.event.release.prerelease || contains(steps.release_version.outputs.release_version, 'M') || contains(steps.release_version.outputs.release_version, 'RC') }} 105 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 106 | BRANCH: gh-pages 107 | FOLDER: build/docs 108 | VERSION: ${{ steps.release_version.outputs.release_version }} 109 | TARGET_REPOSITORY: ${{ github.repository == 'micronaut-projects/micronaut-core' && env.docsRepository || github.repository }} 110 | DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 111 | DEVELOCITY_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }} 112 | DEVELOCITY_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} 113 | - name: Run post-release 114 | if: success() 115 | uses: micronaut-projects/github-actions/post-release@master 116 | env: 117 | MICRONAUT_BUILD_EMAIL: ${{ secrets.MICRONAUT_BUILD_EMAIL }} 118 | with: 119 | token: ${{ secrets.GITHUB_TOKEN }} 120 | 121 | provenance-subject: 122 | needs: [release] 123 | runs-on: ubuntu-latest 124 | outputs: 125 | artifacts-sha256: ${{ steps.set-hash.outputs.artifacts-sha256 }} 126 | steps: 127 | - name: Download artifacts-sha256 128 | uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 129 | with: 130 | name: artifacts-sha256 131 | # The SLSA provenance generator expects the hash digest of artifacts to be passed as a job 132 | # output. So we need to download the artifacts-sha256 and set it as job output. The hash of 133 | # the artifacts should be set as output directly in the release job. But due to a known bug 134 | # in GitHub Actions we have to use a workaround. 135 | # See https://github.com/community/community/discussions/37942. 136 | - name: Set artifacts-sha256 as output 137 | id: set-hash 138 | shell: bash 139 | run: echo "artifacts-sha256=$(cat artifacts-sha256)" >> "$GITHUB_OUTPUT" 140 | 141 | provenance: 142 | needs: [release, provenance-subject] 143 | permissions: 144 | actions: read # To read the workflow path. 145 | id-token: write # To sign the provenance. 146 | contents: write # To add assets to a release. 147 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 148 | with: 149 | base64-subjects: "${{ needs.provenance-subject.outputs.artifacts-sha256 }}" 150 | upload-assets: true # Upload to a new release. 151 | compile-generator: true # Build the generator from source. 152 | 153 | github_release: 154 | needs: [release, provenance] 155 | runs-on: ubuntu-latest 156 | if: startsWith(github.ref, 'refs/tags/') 157 | steps: 158 | - name: Checkout repository 159 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 160 | - name: Download artifacts 161 | uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 162 | with: 163 | name: gradle-build-outputs 164 | path: build/repo 165 | - name: Create artifacts archive 166 | shell: bash 167 | run: | 168 | find build/repo -type f \( \( -iname "*.jar" -not -iname "*-javadoc.jar" -not \ 169 | -iname "*-sources.jar" \) -or -iname "*.pom" \) | xargs zip artifacts.zip 170 | - name: Upload assets 171 | # Upload the artifacts to the existing release. Note that the SLSA provenance will 172 | # attest to each artifact file and not the aggregated ZIP file. 173 | uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 174 | with: 175 | files: artifacts.zip 176 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | --------------------------------------------------------------------------------