Skip to content

Commit 6682317

Browse files
committed
Add TrustStore Support for Fetching OpenAPI via HTTPS with Self-Signed Certificates. Fixes #136 #135
1 parent e855f9c commit 6682317

File tree

8 files changed

+109
-18
lines changed

8 files changed

+109
-18
lines changed

README.md

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ Gradle Groovy DSL
2525

2626
```groovy
2727
plugins {
28-
id "org.springframework.boot" version "2.7.0"
29-
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
28+
id "org.springframework.boot" version "2.7.0"
29+
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
3030
}
3131
```
3232

@@ -73,24 +73,30 @@ openApi as follows
7373

7474
```kotlin
7575
openApi {
76-
apiDocsUrl.set("https://localhost:9000/api/docs")
77-
outputDir.set(file("$buildDir/docs"))
78-
outputFileName.set("swagger.json")
79-
waitTimeInSeconds.set(10)
80-
groupedApiMappings.set(["https://localhost:8080/v3/api-docs/groupA" to "swagger-groupA.json",
81-
"https://localhost:8080/v3/api-docs/groupB" to "swagger-groupB.json"])
82-
customBootRun {
83-
args.set(["--spring.profiles.active=special"])
84-
}
76+
apiDocsUrl.set("https://localhost:9000/api/docs")
77+
outputDir.set(file("$buildDir/docs"))
78+
outputFileName.set("swagger.json")
79+
waitTimeInSeconds.set(10)
80+
trustStore.set("keystore/truststore.p12")
81+
trustStorePassword.set("changeit".toCharArray())
82+
groupedApiMappings.set(
83+
["https://localhost:8080/v3/api-docs/groupA" to "swagger-groupA.json",
84+
"https://localhost:8080/v3/api-docs/groupB" to "swagger-groupB.json"]
85+
)
86+
customBootRun {
87+
args.set(["--spring.profiles.active=special"])
88+
}
8589
}
8690
```
8791

8892
| Parameter | Description | Required | Default |
8993
|----------------------|-------------------------------------------------------------------------------------------------------------------------------------|----------|--------------------------------------|
90-
| `apiDocsUrl` | The URL from where the OpenAPI doc can be downloaded. If the url ends with `.yaml`, output will YAML format. | No | http://localhost:8080/v3/api-docs |
94+
| `apiDocsUrl` | The URL from where the OpenAPI doc can be downloaded. If the url ends with `.yaml`, output will YAML format. | No | http://localhost:8080/v3/api-docs |
9195
| `outputDir` | The output directory for the generated OpenAPI file | No | $buildDir - Your project's build dir |
92-
| `outputFileName` | Specifies the output file name. | No | openapi.json |
96+
| `outputFileName` | Specifies the output file name. | No | openapi.json |
9397
| `waitTimeInSeconds` | Time to wait in seconds for your Spring Boot application to start, before we make calls to `apiDocsUrl` to download the OpenAPI doc | No | 30 seconds |
98+
| `trustStore` | Path to a trust store that contains custom trusted certificates. | No | `<None>` |
99+
| `trustStorePassword` | Password to open Trust Store | No | `<None>` |
94100
| `groupedApiMappings` | A map of URLs (from where the OpenAPI docs can be downloaded) to output file names | No | [] |
95101
| `customBootRun` | Any bootRun property that you would normal need to start your spring boot application. | No | (N/A) |
96102

@@ -134,6 +140,20 @@ openApi {
134140
}
135141
```
136142

143+
### Trust Store Configuration
144+
145+
If you have restricted your application to HTTPS only and prefer not to include your certificate
146+
in Java's cacerts file, you can configure your own set of trusted certificates through plugin
147+
properties, ensuring SSL connections are established.
148+
149+
#### Generating a Trust Store
150+
151+
To create your own Trust Store, utilize the Java keytool command:
152+
153+
```shell
154+
keytool -storepass changeit -noprompt -import -alias ca -file [CERT_PATH]/ca.crt -keystore [KEYSTORE_PATH]/truststore.p12 -deststoretype PKCS12
155+
```
156+
137157
### Grouped API Mappings Notes
138158

139159
The `groupedApiMappings` customization allows you to specify multiple URLs/file names for

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins {
99
}
1010

1111
group = "org.springdoc"
12-
version = "1.8.0"
12+
version = "1.9.0"
1313

1414
sonarqube {
1515
properties {

src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiExtension.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ open class OpenApiExtension @Inject constructor(
1818
val outputFileName: Property<String> = objects.property(String::class.java)
1919
val outputDir: DirectoryProperty = objects.directoryProperty()
2020
val waitTimeInSeconds: Property<Int> = objects.property(Int::class.java)
21+
val trustStore: Property<String> = objects.property(String::class.java)
22+
val trustStorePassword: Property<CharArray> = objects.property(CharArray::class.java)
23+
2124
val groupedApiMappings: MapProperty<String, String> =
2225
objects.mapProperty(String::class.java, String::class.java)
2326
val customBootRun: CustomBootRunAction =

src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGeneratorTask.kt

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@ import org.gradle.api.provider.MapProperty
1717
import org.gradle.api.provider.Property
1818
import org.gradle.api.tasks.Input
1919
import org.gradle.api.tasks.Internal
20+
import org.gradle.api.tasks.Optional
2021
import org.gradle.api.tasks.OutputDirectory
2122
import org.gradle.api.tasks.TaskAction
23+
import java.io.FileInputStream
2224
import java.net.ConnectException
2325
import java.net.HttpURLConnection
2426
import java.net.URL
27+
import java.security.KeyStore
28+
import java.security.SecureRandom
2529
import java.time.Duration
2630
import java.time.temporal.ChronoUnit.SECONDS
31+
import java.util.Locale
32+
import javax.net.ssl.HttpsURLConnection
33+
import javax.net.ssl.KeyManager
34+
import javax.net.ssl.SSLContext
35+
import javax.net.ssl.TrustManagerFactory
36+
2737

2838
private const val MAX_HTTP_STATUS_CODE = 299
2939

@@ -40,9 +50,17 @@ open class OpenApiGeneratorTask : DefaultTask() {
4050

4151
@get:OutputDirectory
4252
val outputDir: DirectoryProperty = project.objects.directoryProperty()
53+
4354
@get:Internal
44-
val waitTimeInSeconds: Property<Int> =
45-
project.objects.property(Int::class.java)
55+
val waitTimeInSeconds: Property<Int> = project.objects.property(Int::class.java)
56+
57+
@get:Optional
58+
@get:Input
59+
val trustStore: Property<String> = project.objects.property(String::class.java)
60+
61+
@get:Optional
62+
@get:Input
63+
val trustStorePassword: Property<CharArray> = project.objects.property(CharArray::class.java)
4664

4765
init {
4866
description = OPEN_API_TASK_DESCRIPTION
@@ -56,6 +74,8 @@ open class OpenApiGeneratorTask : DefaultTask() {
5674
groupedApiMappings.convention(extension.groupedApiMappings)
5775
outputDir.convention(extension.outputDir)
5876
waitTimeInSeconds.convention(extension.waitTimeInSeconds)
77+
trustStore.convention(extension.trustStore)
78+
trustStorePassword.convention(extension.trustStorePassword)
5979
}
6080

6181
@TaskAction
@@ -69,20 +89,23 @@ open class OpenApiGeneratorTask : DefaultTask() {
6989

7090
private fun generateApiDocs(url: String, fileName: String) {
7191
try {
72-
val isYaml = url.toLowerCase().matches(Regex(".+[./]yaml(/.+)*"))
92+
val isYaml = url.lowercase(Locale.getDefault()).matches(Regex(".+[./]yaml(/.+)*"))
93+
val sslContext = getCustomSslContext()
7394
await ignoreException ConnectException::class withPollInterval Durations.ONE_SECOND atMost Duration.of(
7495
waitTimeInSeconds.get().toLong(),
7596
SECONDS
7697
) until {
98+
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
7799
val connection: HttpURLConnection =
78100
URL(url).openConnection() as HttpURLConnection
79101
connection.requestMethod = "GET"
80102
connection.connect()
81103
val statusCode = connection.responseCode
82-
logger.trace("apiDocsUrl = {} status code = {}", url, statusCode)
104+
logger.debug("apiDocsUrl = {} status code = {}", url, statusCode)
83105
statusCode < MAX_HTTP_STATUS_CODE
84106
}
85107
logger.info("Generating OpenApi Docs..")
108+
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
86109
val connection: HttpURLConnection =
87110
URL(url).openConnection() as HttpURLConnection
88111
connection.requestMethod = "GET"
@@ -103,6 +126,24 @@ open class OpenApiGeneratorTask : DefaultTask() {
103126
}
104127
}
105128

129+
private fun getCustomSslContext(): SSLContext {
130+
if (trustStore.isPresent) {
131+
logger.debug("Reading truststore: ${trustStore.get()}")
132+
FileInputStream(trustStore.get()).use { truststoreFile ->
133+
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
134+
val truststore = KeyStore.getInstance(KeyStore.getDefaultType())
135+
truststore.load(truststoreFile, trustStorePassword.get())
136+
trustManagerFactory.init(truststore)
137+
val sslContext: SSLContext = SSLContext.getInstance("TLSv1.2")
138+
val keyManagers = arrayOf<KeyManager>()
139+
sslContext.init(keyManagers, trustManagerFactory.trustManagers, SecureRandom())
140+
141+
return sslContext
142+
}
143+
}
144+
return SSLContext.getDefault()
145+
}
146+
106147
private fun prettifyJson(response: String): String {
107148
val gson = GsonBuilder().setPrettyPrinting().create()
108149
try {

src/test/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGradlePluginTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,27 @@ class OpenApiGradlePluginTest {
224224
assertOpenApiJsonFile(1)
225225
}
226226

227+
@Test
228+
fun `using HTTPS api url to download api-docs`() {
229+
val trustStore = File(projectTestDir, "truststore.p12")
230+
buildFile.writeText(
231+
"""$baseBuildGradle
232+
233+
openApi{
234+
trustStore = "${trustStore.absolutePath}"
235+
trustStorePassword = "changeit".toCharArray()
236+
apiDocsUrl = "https://127.0.0.1:8081/v3/api-docs"
237+
customBootRun {
238+
args = ["--spring.profiles.active=ssl"]
239+
}
240+
}
241+
""".trimMargin()
242+
)
243+
244+
assertEquals(TaskOutcome.SUCCESS, openApiDocsTask(runTheBuild()).outcome)
245+
assertOpenApiJsonFile(1)
246+
}
247+
227248
@Test
228249
fun `yaml generation`() {
229250
val outputYamlFileName = "openapi.yaml"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
server.port=8081
2+
server.ssl.key-alias=ssl
3+
server.ssl.key-password=+bAyoiVYOy6Tg/v2IG4blme2Hu+ORTksvFh/w9s=
4+
server.ssl.key-store=classpath:keystore.p12
5+
server.ssl.key-store-password=+bAyoiVYOy6Tg/v2IG4blme2Hu+ORTksvFh/w9s=
6+
server.ssl.key-store-type=PKCS12
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)