diff --git a/README.md b/README.md index db6bb9a..02e6afe 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ Gradle Groovy DSL ```groovy plugins { - id "org.springframework.boot" version "2.7.0" - id "org.springdoc.openapi-gradle-plugin" version "1.8.0" + id "org.springframework.boot" version "2.7.0" + id "org.springdoc.openapi-gradle-plugin" version "1.8.0" } ``` @@ -73,24 +73,30 @@ openApi as follows ```kotlin openApi { - apiDocsUrl.set("https://localhost:9000/api/docs") - outputDir.set(file("$buildDir/docs")) - outputFileName.set("swagger.json") - waitTimeInSeconds.set(10) - groupedApiMappings.set(["https://localhost:8080/v3/api-docs/groupA" to "swagger-groupA.json", - "https://localhost:8080/v3/api-docs/groupB" to "swagger-groupB.json"]) - customBootRun { - args.set(["--spring.profiles.active=special"]) - } + apiDocsUrl.set("https://localhost:9000/api/docs") + outputDir.set(file("$buildDir/docs")) + outputFileName.set("swagger.json") + waitTimeInSeconds.set(10) + trustStore.set("keystore/truststore.p12") + trustStorePassword.set("changeit".toCharArray()) + groupedApiMappings.set( + ["https://localhost:8080/v3/api-docs/groupA" to "swagger-groupA.json", + "https://localhost:8080/v3/api-docs/groupB" to "swagger-groupB.json"] + ) + customBootRun { + args.set(["--spring.profiles.active=special"]) + } } ``` | Parameter | Description | Required | Default | |----------------------|-------------------------------------------------------------------------------------------------------------------------------------|----------|--------------------------------------| -| `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 | +| `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 | | `outputDir` | The output directory for the generated OpenAPI file | No | $buildDir - Your project's build dir | -| `outputFileName` | Specifies the output file name. | No | openapi.json | +| `outputFileName` | Specifies the output file name. | No | openapi.json | | `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 | +| `trustStore` | Path to a trust store that contains custom trusted certificates. | No | `` | +| `trustStorePassword` | Password to open Trust Store | No | `` | | `groupedApiMappings` | A map of URLs (from where the OpenAPI docs can be downloaded) to output file names | No | [] | | `customBootRun` | Any bootRun property that you would normal need to start your spring boot application. | No | (N/A) | @@ -134,6 +140,20 @@ openApi { } ``` +### Trust Store Configuration + +If you have restricted your application to HTTPS only and prefer not to include your certificate +in Java's cacerts file, you can configure your own set of trusted certificates through plugin +properties, ensuring SSL connections are established. + +#### Generating a Trust Store + +To create your own Trust Store, utilize the Java keytool command: + +```shell +keytool -storepass changeit -noprompt -import -alias ca -file [CERT_PATH]/ca.crt -keystore [KEYSTORE_PATH]/truststore.p12 -deststoretype PKCS12 +``` + ### Grouped API Mappings Notes The `groupedApiMappings` customization allows you to specify multiple URLs/file names for diff --git a/src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiExtension.kt b/src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiExtension.kt index 47eb8c5..79bca44 100644 --- a/src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiExtension.kt +++ b/src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiExtension.kt @@ -18,6 +18,9 @@ open class OpenApiExtension @Inject constructor( val outputFileName: Property = objects.property(String::class.java) val outputDir: DirectoryProperty = objects.directoryProperty() val waitTimeInSeconds: Property = objects.property(Int::class.java) + val trustStore: Property = objects.property(String::class.java) + val trustStorePassword: Property = objects.property(CharArray::class.java) + val groupedApiMappings: MapProperty = objects.mapProperty(String::class.java, String::class.java) val customBootRun: CustomBootRunAction = diff --git a/src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGeneratorTask.kt b/src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGeneratorTask.kt index 316b8a8..d35cc46 100644 --- a/src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGeneratorTask.kt +++ b/src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGeneratorTask.kt @@ -17,13 +17,23 @@ import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction +import java.io.FileInputStream import java.net.ConnectException import java.net.HttpURLConnection import java.net.URL +import java.security.KeyStore +import java.security.SecureRandom import java.time.Duration import java.time.temporal.ChronoUnit.SECONDS +import java.util.Locale +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.KeyManager +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory + private const val MAX_HTTP_STATUS_CODE = 299 @@ -40,9 +50,17 @@ open class OpenApiGeneratorTask : DefaultTask() { @get:OutputDirectory val outputDir: DirectoryProperty = project.objects.directoryProperty() + @get:Internal - val waitTimeInSeconds: Property = - project.objects.property(Int::class.java) + val waitTimeInSeconds: Property = project.objects.property(Int::class.java) + + @get:Optional + @get:Input + val trustStore: Property = project.objects.property(String::class.java) + + @get:Optional + @get:Input + val trustStorePassword: Property = project.objects.property(CharArray::class.java) init { description = OPEN_API_TASK_DESCRIPTION @@ -56,6 +74,8 @@ open class OpenApiGeneratorTask : DefaultTask() { groupedApiMappings.convention(extension.groupedApiMappings) outputDir.convention(extension.outputDir) waitTimeInSeconds.convention(extension.waitTimeInSeconds) + trustStore.convention(extension.trustStore) + trustStorePassword.convention(extension.trustStorePassword) } @TaskAction @@ -69,20 +89,23 @@ open class OpenApiGeneratorTask : DefaultTask() { private fun generateApiDocs(url: String, fileName: String) { try { - val isYaml = url.toLowerCase().matches(Regex(".+[./]yaml(/.+)*")) + val isYaml = url.lowercase(Locale.getDefault()).matches(Regex(".+[./]yaml(/.+)*")) + val sslContext = getCustomSslContext() await ignoreException ConnectException::class withPollInterval Durations.ONE_SECOND atMost Duration.of( waitTimeInSeconds.get().toLong(), SECONDS ) until { + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) val connection: HttpURLConnection = URL(url).openConnection() as HttpURLConnection connection.requestMethod = "GET" connection.connect() val statusCode = connection.responseCode - logger.trace("apiDocsUrl = {} status code = {}", url, statusCode) + logger.debug("apiDocsUrl = {} status code = {}", url, statusCode) statusCode < MAX_HTTP_STATUS_CODE } logger.info("Generating OpenApi Docs..") + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) val connection: HttpURLConnection = URL(url).openConnection() as HttpURLConnection connection.requestMethod = "GET" @@ -103,6 +126,24 @@ open class OpenApiGeneratorTask : DefaultTask() { } } + private fun getCustomSslContext(): SSLContext { + if (trustStore.isPresent) { + logger.debug("Reading truststore: ${trustStore.get()}") + FileInputStream(trustStore.get()).use { truststoreFile -> + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + val truststore = KeyStore.getInstance(KeyStore.getDefaultType()) + truststore.load(truststoreFile, trustStorePassword.get()) + trustManagerFactory.init(truststore) + val sslContext: SSLContext = SSLContext.getInstance("TLSv1.2") + val keyManagers = arrayOf() + sslContext.init(keyManagers, trustManagerFactory.trustManagers, SecureRandom()) + + return sslContext + } + } + return SSLContext.getDefault() + } + private fun prettifyJson(response: String): String { val gson = GsonBuilder().setPrettyPrinting().create() try { diff --git a/src/test/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGradlePluginTest.kt b/src/test/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGradlePluginTest.kt index 805d3c3..8c659a3 100644 --- a/src/test/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGradlePluginTest.kt +++ b/src/test/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGradlePluginTest.kt @@ -224,6 +224,27 @@ class OpenApiGradlePluginTest { assertOpenApiJsonFile(1) } + @Test + fun `using HTTPS api url to download api-docs`() { + val trustStore = File(projectTestDir, "truststore.p12") + buildFile.writeText( + """$baseBuildGradle + + openApi{ + trustStore = "${trustStore.absolutePath}" + trustStorePassword = "changeit".toCharArray() + apiDocsUrl = "https://127.0.0.1:8081/v3/api-docs" + customBootRun { + args = ["--spring.profiles.active=ssl"] + } + } + """.trimMargin() + ) + + assertEquals(TaskOutcome.SUCCESS, openApiDocsTask(runTheBuild()).outcome) + assertOpenApiJsonFile(1) + } + @Test fun `yaml generation`() { val outputYamlFileName = "openapi.yaml" diff --git a/src/test/resources/acceptance-project/src/main/resources/application-ssl.properties b/src/test/resources/acceptance-project/src/main/resources/application-ssl.properties new file mode 100644 index 0000000..0e36a70 --- /dev/null +++ b/src/test/resources/acceptance-project/src/main/resources/application-ssl.properties @@ -0,0 +1,6 @@ +server.port=8081 +server.ssl.key-alias=ssl +server.ssl.key-password=+bAyoiVYOy6Tg/v2IG4blme2Hu+ORTksvFh/w9s= +server.ssl.key-store=classpath:keystore.p12 +server.ssl.key-store-password=+bAyoiVYOy6Tg/v2IG4blme2Hu+ORTksvFh/w9s= +server.ssl.key-store-type=PKCS12 diff --git a/src/test/resources/acceptance-project/src/main/resources/keystore.p12 b/src/test/resources/acceptance-project/src/main/resources/keystore.p12 new file mode 100644 index 0000000..f9ee31b Binary files /dev/null and b/src/test/resources/acceptance-project/src/main/resources/keystore.p12 differ diff --git a/src/test/resources/acceptance-project/truststore.p12 b/src/test/resources/acceptance-project/truststore.p12 new file mode 100644 index 0000000..1161456 Binary files /dev/null and b/src/test/resources/acceptance-project/truststore.p12 differ