├── 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 | [](https://search.maven.org/search?q=g:%22io.micronaut.acme%22%20AND%20a:%22micronaut-acme%22)
4 | [](https://github.com/micronaut-projects/micronaut-acme/actions)
5 | [](https://sonarcloud.io/summary/new_code?id=micronaut-projects_micronaut-acme)
6 | [](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 | 
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 | 
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 | 
26 | 1. Next create a new record set in Route 53 using the public ipv4 address for your EC2 server
27 | 
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. 
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 |
--------------------------------------------------------------------------------