diff --git a/foojay-resolver/build.gradle.kts b/foojay-resolver/build.gradle.kts index c6d71fa..ad4d769 100644 --- a/foojay-resolver/build.gradle.kts +++ b/foojay-resolver/build.gradle.kts @@ -104,6 +104,7 @@ testing { val functionalTest by registering(JvmTestSuite::class) { dependencies { implementation("org.jetbrains.kotlin:kotlin-test-junit5") + implementation(libs.littleproxy) } } val test by getting(JvmTestSuite::class) { diff --git a/foojay-resolver/src/functionalTest/kotlin/org/gradle/toolchains/foojay/AbstractFoojayToolchainsPluginFunctionalTest.kt b/foojay-resolver/src/functionalTest/kotlin/org/gradle/toolchains/foojay/AbstractFoojayToolchainsPluginFunctionalTest.kt index 06f957e..41b197f 100644 --- a/foojay-resolver/src/functionalTest/kotlin/org/gradle/toolchains/foojay/AbstractFoojayToolchainsPluginFunctionalTest.kt +++ b/foojay-resolver/src/functionalTest/kotlin/org/gradle/toolchains/foojay/AbstractFoojayToolchainsPluginFunctionalTest.kt @@ -1,11 +1,21 @@ package org.gradle.toolchains.foojay +import io.netty.handler.codec.http.HttpObject +import io.netty.handler.codec.http.HttpRequest +import io.netty.handler.codec.http.HttpResponse import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.io.TempDir +import org.littleshoot.proxy.HttpFilters +import org.littleshoot.proxy.HttpFiltersAdapter +import org.littleshoot.proxy.HttpFiltersSourceAdapter +import org.littleshoot.proxy.HttpProxyServer +import org.littleshoot.proxy.impl.DefaultHttpProxyServer import java.io.File +import java.util.concurrent.atomic.AtomicInteger import kotlin.test.assertTrue abstract class AbstractFoojayToolchainsPluginFunctionalTest { @@ -16,6 +26,10 @@ abstract class AbstractFoojayToolchainsPluginFunctionalTest { @field:TempDir protected lateinit var homeDir: File + protected lateinit var proxyServer: HttpProxyServer + protected var proxyPort: Int = 0 + protected val proxyInterceptorCount = AtomicInteger(0) + private val settingsFile by lazy { projectDir.resolve("settings.gradle.kts") } private val propertiesFile by lazy { projectDir.resolve("gradle.properties") } private val buildFile by lazy { projectDir.resolve("build.gradle.kts") } @@ -27,9 +41,38 @@ abstract class AbstractFoojayToolchainsPluginFunctionalTest { org.gradle.java.installations.auto-detect=false org.gradle.java.installations.auto-download=true """.trimIndent()) + // Start proxy on a random available port + proxyInterceptorCount.set(0) + proxyServer = DefaultHttpProxyServer.bootstrap() + .withPort(0) + .withFiltersSource(object : HttpFiltersSourceAdapter() { + override fun filterRequest(originalRequest: HttpRequest): HttpFilters { + return object : HttpFiltersAdapter(originalRequest) { + override fun clientToProxyRequest(httpObject: HttpObject): HttpResponse? { + // Increment whenever the proxy intercepts a request + if (httpObject is HttpRequest) { + proxyInterceptorCount.incrementAndGet() + } + return null // Continue normal routing + } + } + } + }) + .start() + proxyPort = proxyServer.listenAddress.port } - protected fun runner(settings: String, buildScript: String): GradleRunner { + @AfterEach + internal fun tearDownProxy() { + proxyServer.stop() + } + + protected fun runner( + settings: String, + buildScript: String, + extraArguments: List = emptyList() + ): GradleRunner { + settingsFile.writeText(settings) buildFile.writeText(buildScript.trimIndent()) @@ -46,7 +89,7 @@ abstract class AbstractFoojayToolchainsPluginFunctionalTest { return GradleRunner.create() .forwardOutput() .withPluginClasspath() - .withArguments(listOf("--info", "-g", homeDir.absolutePath, "compileJava")) + .withArguments(listOf("--info", "-g", homeDir.absolutePath, "compileJava") + extraArguments) .withProjectDir(projectDir) } diff --git a/foojay-resolver/src/functionalTest/kotlin/org/gradle/toolchains/foojay/FoojayToolchainsPluginFunctionalTest.kt b/foojay-resolver/src/functionalTest/kotlin/org/gradle/toolchains/foojay/FoojayToolchainsPluginFunctionalTest.kt index 12befa8..a91d645 100644 --- a/foojay-resolver/src/functionalTest/kotlin/org/gradle/toolchains/foojay/FoojayToolchainsPluginFunctionalTest.kt +++ b/foojay-resolver/src/functionalTest/kotlin/org/gradle/toolchains/foojay/FoojayToolchainsPluginFunctionalTest.kt @@ -3,9 +3,10 @@ package org.gradle.toolchains.foojay import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue -class FoojayToolchainsPluginFunctionalTest: AbstractFoojayToolchainsPluginFunctionalTest() { +class FoojayToolchainsPluginFunctionalTest : AbstractFoojayToolchainsPluginFunctionalTest() { @ParameterizedTest(name = "gradle version: {0}") @MethodSource("getGradleTestVersions") @@ -43,6 +44,50 @@ class FoojayToolchainsPluginFunctionalTest: AbstractFoojayToolchainsPluginFuncti assertProvisioningSuccessful(result) } + @ParameterizedTest(name = "gradle version: {0}") + @MethodSource("getGradleTestVersions") + fun `can use base plugin with proxy`(gradleVersion: String) { + val settings = """ + plugins { + id("org.gradle.toolchains.foojay-resolver") + } + + toolchainManagement { + jvm { + javaRepositories { + repository("foojay") { + resolverClass.set(org.gradle.toolchains.foojay.FoojayToolchainResolver::class.java) + } + } + } + } + """.trimIndent() + + val buildScript = """ + plugins { + java + } + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(${getDifferentJavaVersion()})) + } + } + """ + val arguments = listOf( + "-Dhttps.proxyHost=127.0.0.1", + "-Dhttps.proxyPort=$proxyPort", + "-Dhttps.nonProxyHosts=''", + // To make sure Gradle does not pick up local JDK installations. + "-Porg.gradle.java.installations.auto-detect=false" + ) + val result = runner(settings, buildScript, arguments) + .withGradleVersion(gradleVersion) + .build() + assertProvisioningSuccessful(result) + assertTrue(proxyInterceptorCount.get() > 0, "Traffic bypassed the proxy.") + } + @Test fun `generates useful error for unsupported Gradle versions`() { val settings = """ @@ -110,7 +155,9 @@ class FoojayToolchainsPluginFunctionalTest: AbstractFoojayToolchainsPluginFuncti .withGradleVersion(gradleVersion) .buildAndFail() - assertTrue("> Failed to apply plugin 'org.gradle.toolchains.foojay-resolver'.\n" + - " > Settings plugins must be applied in the settings script." in result.output) + assertTrue( + "> Failed to apply plugin 'org.gradle.toolchains.foojay-resolver'.\n" + + " > Settings plugins must be applied in the settings script." in result.output + ) } } diff --git a/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayApi.kt b/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayApi.kt index 423802d..53a5bb6 100644 --- a/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayApi.kt +++ b/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayApi.kt @@ -6,19 +6,18 @@ import org.gradle.jvm.toolchain.JvmImplementation import org.gradle.jvm.toolchain.JvmVendorSpec import org.gradle.platform.Architecture import org.gradle.platform.OperatingSystem -import java.io.BufferedReader -import java.io.InputStream -import java.net.HttpURLConnection -import java.net.URL +import java.net.URI import java.net.URLEncoder +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse import java.nio.charset.StandardCharsets.UTF_8 -import java.util.concurrent.TimeUnit.SECONDS +import java.time.Duration @Suppress("MagicNumber") -private val CONNECT_TIMEOUT = SECONDS.toMillis(10).toInt() - +private val CONNECT_TIMEOUT = Duration.ofSeconds(10) @Suppress("MagicNumber") -private val READ_TIMEOUT = SECONDS.toMillis(20).toInt() +private val READ_TIMEOUT = Duration.ofSeconds(20) private const val SCHEMA = "https" @@ -31,6 +30,10 @@ class FoojayApi { private val distributions = mutableListOf() + private val httpClient = HttpClient.newBuilder() + .connectTimeout(CONNECT_TIMEOUT) + .build() + @Suppress("LongParameterList") fun toPackage( version: JavaLanguageVersion, @@ -53,13 +56,10 @@ class FoojayApi { private fun fetchDistributionsIfMissing() { if (distributions.isEmpty()) { - val con = createConnection( + val json = downloadVendorList( DISTRIBUTIONS_ENDPOINT, mapOf("include_versions" to "true", "include_synonyms" to "true") ) - val json = readResponse(con) - con.disconnect() - distributions.addAll(parseDistributions(json)) } } @@ -70,8 +70,7 @@ class FoojayApi { distributionName == "graalvm" -> "version" else -> "jdk_version" } - - val con = createConnection( + val json = downloadVendorList( PACKAGES_ENDPOINT, mapOf( versionApiKey to "$version", @@ -81,21 +80,27 @@ class FoojayApi { "directly_downloadable" to "true" ) ) - val json = readResponse(con) - con.disconnect() - val packages = parsePackages(json) return match(packages, architecture) } - private fun createConnection(endpoint: String, parameters: Map): HttpURLConnection { - val url = URL("$SCHEMA://$endpoint?${toParameterString(parameters)}") - val con = url.openConnection() as HttpURLConnection - con.setRequestProperty("Content-Type", "application/json") - con.requestMethod = "GET" - con.connectTimeout = CONNECT_TIMEOUT - con.readTimeout = READ_TIMEOUT - return con + @Suppress("MagicNumber") + private fun downloadVendorList( + endpoint: String, + params: Map + ): String { + val uri = URI.create("$SCHEMA://$endpoint?${toParameterString(params)}") + val request = HttpRequest.newBuilder() + .uri(uri) + .header("Content-Type", "application/json") + .timeout(READ_TIMEOUT) + .GET() + .build() + val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) + if (response.statusCode() != 200) { + throw GradleException("Requesting vendor list failed: ${response.body()}") + } + return response.body() } private fun toParameterString(params: Map): String { @@ -103,14 +108,4 @@ class FoojayApi { "${URLEncoder.encode(it.key, UTF_8.name())}=${URLEncoder.encode(it.value, UTF_8.name())}" } } - - private fun readResponse(con: HttpURLConnection): String { - val status = con.responseCode - if (status != HttpURLConnection.HTTP_OK) { - throw GradleException("Requesting vendor list failed: ${readContent(con.errorStream)}") - } - return readContent(con.inputStream) - } - - private fun readContent(stream: InputStream) = stream.bufferedReader().use(BufferedReader::readText) } diff --git a/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayToolchainResolver.kt b/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayToolchainResolver.kt index f44eefe..84ff12c 100644 --- a/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayToolchainResolver.kt +++ b/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayToolchainResolver.kt @@ -8,9 +8,10 @@ import org.gradle.jvm.toolchain.JavaToolchainSpec import org.gradle.util.GradleVersion import java.util.* +@Suppress("UnstableApiUsage") abstract class FoojayToolchainResolver : JavaToolchainResolver { - private val api: FoojayApi = FoojayApi() + private val api = FoojayApi() override fun resolve(request: JavaToolchainRequest): Optional { val spec = request.javaToolchainSpec diff --git a/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayToolchainsPlugin.kt b/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayToolchainsPlugin.kt index b63a7c4..84301cc 100644 --- a/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayToolchainsPlugin.kt +++ b/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/FoojayToolchainsPlugin.kt @@ -4,11 +4,11 @@ package org.gradle.toolchains.foojay import org.gradle.api.initialization.Settings -import org.gradle.api.internal.SettingsInternal import org.gradle.jvm.toolchain.JavaToolchainResolverRegistry +import org.gradle.kotlin.dsl.support.serviceOf import org.gradle.util.GradleVersion -@Suppress("unused") +@Suppress("unused", "UnstableApiUsage") abstract class FoojayToolchainsPlugin: AbstractFoojayToolchainPlugin() { @Suppress("TooGenericExceptionThrown") @@ -16,11 +16,8 @@ abstract class FoojayToolchainsPlugin: AbstractFoojayToolchainPlugin() { if (GradleVersion.current().baseVersion < GradleVersion.version("7.6")) { throw RuntimeException("${FoojayToolchainsPlugin::class.simpleName} needs Gradle version 7.6 or higher") } - settings.plugins.apply("jvm-toolchain-management") - - val registry = (settings as SettingsInternal).services.get(JavaToolchainResolverRegistry::class.java) - registry.register(FoojayToolchainResolver::class.java) + settings.serviceOf() + .apply { register(FoojayToolchainResolver::class.java) } } - } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 27306c0..4fc7d05 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ [libraries] gson = { module = "com.google.code.gson:gson", version = "2.14.0" } +littleproxy = { module = "io.github.littleproxy:littleproxy", version = "2.7.0" } [plugins] plugin-publish = { id = "com.gradle.plugin-publish", version = "2.1.1" }