mirror of
https://github.com/apple/pkl.git
synced 2026-07-08 05:55:13 +02:00
Improve handling of CA certificates (#518)
Instead of bundling Pkl's built-in CA certificates as a class path resource and loading them at runtime, pass them to the native image compiler as the default SSL context's trust store. This results in faster SSL initialization and is more consistent with how default certificates are handled when running on the JVM. Further related improvements: - Remove HttpClientBuilder methods `addDefaultCliCertificates` and `addBuiltInCertificates`. - Remove pkl-certs subproject and the optional dependencies on it. - Move `PklCARoots.pem` to `pkl-cli/src/certs`. - Fix certificate related error messages that were missing an argument. - Prevent PklBugException if initialization of `CliBaseOptions.httpClient` fails. - Add ability to set CA certificates as a byte array - Add CA certificates option to message passing API
This commit is contained in:
@@ -121,7 +121,7 @@ outputFormat: String?
|
|||||||
/// The project dependency settings.
|
/// The project dependency settings.
|
||||||
project: Project?
|
project: Project?
|
||||||
|
|
||||||
/// Configuration of outgoing HTTP requests.
|
/// Configuration of outgoing HTTP(s) requests.
|
||||||
http: Http?
|
http: Http?
|
||||||
|
|
||||||
class ClientResourceReader {
|
class ClientResourceReader {
|
||||||
@@ -178,8 +178,27 @@ class Project {
|
|||||||
dependencies: Mapping<String, Project|RemoteDependency>
|
dependencies: Mapping<String, Project|RemoteDependency>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings that control how Pkl talks to HTTP(S) servers.
|
class RemoteDependency {
|
||||||
|
type: "remote"
|
||||||
|
|
||||||
|
/// The canonical URI of this dependency
|
||||||
|
packageUri: String?
|
||||||
|
|
||||||
|
/// The checksums of this remote dependency
|
||||||
|
checksums: Checksums?
|
||||||
|
}
|
||||||
|
|
||||||
|
class Checksums {
|
||||||
|
/// The sha-256 checksum of this dependency's metadata.
|
||||||
|
sha256: String
|
||||||
|
}
|
||||||
|
|
||||||
class Http {
|
class Http {
|
||||||
|
/// PEM format certificates to trust when making HTTP requests.
|
||||||
|
///
|
||||||
|
/// If [null], Pkl will trust its own built-in certificates.
|
||||||
|
caCertificates: Binary?
|
||||||
|
|
||||||
/// Configuration of the HTTP proxy to use.
|
/// Configuration of the HTTP proxy to use.
|
||||||
///
|
///
|
||||||
/// If [null], uses the operating system's proxy configuration.
|
/// If [null], uses the operating system's proxy configuration.
|
||||||
@@ -226,21 +245,9 @@ class Proxy {
|
|||||||
noProxy: Listing<String>(isDistinct)
|
noProxy: Listing<String>(isDistinct)
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteDependency {
|
typealias Binary = Any // <1>
|
||||||
type: "remote"
|
|
||||||
|
|
||||||
/// The canonical URI of this dependency
|
|
||||||
packageUri: String?
|
|
||||||
|
|
||||||
/// The checksums of this remote dependency
|
|
||||||
checksums: Checksums?
|
|
||||||
}
|
|
||||||
|
|
||||||
class Checksums {
|
|
||||||
/// The sha-256 checksum of this dependency's metadata.
|
|
||||||
sha256: String
|
|
||||||
}
|
|
||||||
----
|
----
|
||||||
|
<1> link:{uri-messagepack-bin}[bin format] (not expressable in Pkl)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
[source,json5]
|
[source,json5]
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
plugins {
|
|
||||||
pklAllProjects
|
|
||||||
pklJavaLibrary
|
|
||||||
pklPublishLibrary
|
|
||||||
}
|
|
||||||
|
|
||||||
publishing {
|
|
||||||
publications {
|
|
||||||
named<MavenPublication>("library") {
|
|
||||||
pom {
|
|
||||||
url.set("https://github.com/apple/pkl/tree/main/pkl-certs")
|
|
||||||
description.set("""
|
|
||||||
Pkl's built-in CA certificates.
|
|
||||||
Used by Pkl CLIs and optionally supported by pkl-core.")
|
|
||||||
""".trimIndent())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
pklAllProjects
|
pklAllProjects
|
||||||
pklKotlinLibrary
|
pklKotlinLibrary
|
||||||
@@ -35,6 +38,8 @@ val stagedLinuxAarch64Executable: Configuration by configurations.creating
|
|||||||
val stagedAlpineLinuxAmd64Executable: Configuration by configurations.creating
|
val stagedAlpineLinuxAmd64Executable: Configuration by configurations.creating
|
||||||
val stagedWindowsAmd64Executable: Configuration by configurations.creating
|
val stagedWindowsAmd64Executable: Configuration by configurations.creating
|
||||||
|
|
||||||
|
val certs: SourceSet by sourceSets.creating
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(libs.svm)
|
compileOnly(libs.svm)
|
||||||
|
|
||||||
@@ -142,11 +147,38 @@ tasks.check {
|
|||||||
dependsOn(testStartJavaExecutable)
|
dependsOn(testStartJavaExecutable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val trustStore = layout.buildDirectory.dir("generateTrustStore/PklCARoots.p12")
|
||||||
|
val trustStorePassword = "password" // no sensitive data to protect
|
||||||
|
|
||||||
|
// generate a trust store for Pkl's built-in CA certificates
|
||||||
|
val generateTrustStore by tasks.registering {
|
||||||
|
inputs.file(certs.resources.singleFile)
|
||||||
|
outputs.file(trustStore)
|
||||||
|
doLast {
|
||||||
|
val certificates = certs.resources.singleFile.inputStream().use { stream ->
|
||||||
|
CertificateFactory.getInstance("X.509").generateCertificates(stream)
|
||||||
|
}
|
||||||
|
KeyStore.getInstance("PKCS12").apply {
|
||||||
|
load(null, trustStorePassword.toCharArray()) // initialize empty trust store
|
||||||
|
for ((index, certificate) in certificates.withIndex()) {
|
||||||
|
setCertificateEntry("cert-$index", certificate)
|
||||||
|
}
|
||||||
|
val trustStoreFile = trustStore.get().asFile
|
||||||
|
trustStoreFile.parentFile.mkdirs()
|
||||||
|
trustStoreFile.outputStream().use { stream ->
|
||||||
|
store(stream, trustStorePassword.toCharArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Exec.configureExecutable(
|
fun Exec.configureExecutable(
|
||||||
graalVm: BuildInfo.GraalVm,
|
graalVm: BuildInfo.GraalVm,
|
||||||
outputFile: Provider<RegularFile>,
|
outputFile: Provider<RegularFile>,
|
||||||
extraArgs: List<String> = listOf()
|
extraArgs: List<String> = listOf()
|
||||||
) {
|
) {
|
||||||
|
dependsOn(generateTrustStore)
|
||||||
|
|
||||||
inputs.files(sourceSets.main.map { it.output })
|
inputs.files(sourceSets.main.map { it.output })
|
||||||
.withPropertyName("mainSourceSets")
|
.withPropertyName("mainSourceSets")
|
||||||
.withPathSensitivity(PathSensitivity.RELATIVE)
|
.withPathSensitivity(PathSensitivity.RELATIVE)
|
||||||
@@ -175,9 +207,13 @@ fun Exec.configureExecutable(
|
|||||||
// needed for messagepack-java (see https://github.com/msgpack/msgpack-java/issues/600)
|
// needed for messagepack-java (see https://github.com/msgpack/msgpack-java/issues/600)
|
||||||
,"--initialize-at-run-time=org.msgpack.core.buffer.DirectBufferAccess"
|
,"--initialize-at-run-time=org.msgpack.core.buffer.DirectBufferAccess"
|
||||||
,"--no-fallback"
|
,"--no-fallback"
|
||||||
|
,"-Djavax.net.ssl.trustStore=${trustStore.get().asFile}"
|
||||||
|
,"-Djavax.net.ssl.trustStorePassword=$trustStorePassword"
|
||||||
|
,"-Djavax.net.ssl.trustStoreType=PKCS12"
|
||||||
|
// security property "ocsp.enable=true" is set in Main.kt
|
||||||
|
,"-Dcom.sun.net.ssl.checkRevocation=true"
|
||||||
,"-H:IncludeResources=org/pkl/core/stdlib/.*\\.pkl"
|
,"-H:IncludeResources=org/pkl/core/stdlib/.*\\.pkl"
|
||||||
,"-H:IncludeResources=org/jline/utils/.*"
|
,"-H:IncludeResources=org/jline/utils/.*"
|
||||||
,"-H:IncludeResources=org/pkl/certs/PklCARoots.pem"
|
|
||||||
,"-H:IncludeResourceBundles=org.pkl.core.errorMessages"
|
,"-H:IncludeResourceBundles=org.pkl.core.errorMessages"
|
||||||
,"--macro:truffle"
|
,"--macro:truffle"
|
||||||
,"-H:Class=org.pkl.cli.Main"
|
,"-H:Class=org.pkl.cli.Main"
|
||||||
|
|||||||
@@ -1260,10 +1260,7 @@ result = someLib.x
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `gives decent error message if CLI doesn't have the required CA certificate`() {
|
fun `gives decent error message if CLI doesn't have the required CA certificate`() {
|
||||||
// provide SOME certs to prevent CliEvaluator from falling back to ~/.pkl/cacerts
|
val err = assertThrows<CliException> { evalModuleThatImportsPackage(null, packageServer.port) }
|
||||||
val builtInCerts = FileTestUtils.writePklBuiltInCertificates(tempDir)
|
|
||||||
val err =
|
|
||||||
assertThrows<CliException> { evalModuleThatImportsPackage(builtInCerts, packageServer.port) }
|
|
||||||
assertThat(err)
|
assertThat(err)
|
||||||
.hasMessageContaining("Error during SSL handshake with host `localhost`:")
|
.hasMessageContaining("Error during SSL handshake with host `localhost`:")
|
||||||
.hasMessageContaining("unable to find valid certification path to requested target")
|
.hasMessageContaining("unable to find valid certification path to requested target")
|
||||||
@@ -1460,7 +1457,7 @@ result = someLib.x
|
|||||||
assertThat(output).isEqualTo("result = 1\n")
|
assertThat(output).isEqualTo("result = 1\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun evalModuleThatImportsPackage(certsFile: Path, testPort: Int = -1) {
|
private fun evalModuleThatImportsPackage(certsFile: Path?, testPort: Int = -1) {
|
||||||
val moduleUri =
|
val moduleUri =
|
||||||
writePklFile(
|
writePklFile(
|
||||||
"test.pkl",
|
"test.pkl",
|
||||||
@@ -1475,7 +1472,7 @@ result = someLib.x
|
|||||||
CliEvaluatorOptions(
|
CliEvaluatorOptions(
|
||||||
CliBaseOptions(
|
CliBaseOptions(
|
||||||
sourceModules = listOf(moduleUri),
|
sourceModules = listOf(moduleUri),
|
||||||
caCertificates = listOf(certsFile),
|
caCertificates = buildList { if (certsFile != null) add(certsFile) },
|
||||||
workingDir = tempDir,
|
workingDir = tempDir,
|
||||||
noCache = true,
|
noCache = true,
|
||||||
testPort = testPort
|
testPort = testPort
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(projects.pklCommons)
|
implementation(projects.pklCommons)
|
||||||
testImplementation(projects.pklCommonsTest)
|
testImplementation(projects.pklCommonsTest)
|
||||||
runtimeOnly(projects.pklCerts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
|
|||||||
@@ -15,8 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.pkl.commons.cli
|
package org.pkl.commons.cli
|
||||||
|
|
||||||
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import kotlin.io.path.isRegularFile
|
||||||
import org.pkl.core.*
|
import org.pkl.core.*
|
||||||
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
|
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
|
||||||
import org.pkl.core.http.HttpClient
|
import org.pkl.core.http.HttpClient
|
||||||
@@ -166,6 +168,13 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
|||||||
cliOptions.noProxy
|
cliOptions.noProxy
|
||||||
?: project?.evaluatorSettings?.http?.proxy?.noProxy ?: settings.http?.proxy?.noProxy
|
?: project?.evaluatorSettings?.http?.proxy?.noProxy ?: settings.http?.proxy?.noProxy
|
||||||
|
|
||||||
|
private fun HttpClient.Builder.addDefaultCliCertificates() {
|
||||||
|
val caCertsDir = IoUtils.getPklHomeDir().resolve("cacerts")
|
||||||
|
if (Files.isDirectory(caCertsDir)) {
|
||||||
|
Files.list(caCertsDir).filter { it.isRegularFile() }.forEach { addCertificates(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP client used for this command.
|
* The HTTP client used for this command.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
package org.pkl.commons.cli
|
package org.pkl.commons.cli
|
||||||
|
|
||||||
import java.io.PrintStream
|
import java.io.PrintStream
|
||||||
|
import java.security.Security
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
/** Building block for CLIs. Intended to be called from a `main` method. */
|
/** Building block for CLIs. Intended to be called from a `main` method. */
|
||||||
@@ -29,6 +30,8 @@ fun cliMain(block: () -> Unit) {
|
|||||||
|
|
||||||
// Force `native-image` to use system proxies (which does not happen with `-D`).
|
// Force `native-image` to use system proxies (which does not happen with `-D`).
|
||||||
System.setProperty("java.net.useSystemProxies", "true")
|
System.setProperty("java.net.useSystemProxies", "true")
|
||||||
|
// enable OCSP for default SSL context
|
||||||
|
Security.setProperty("ocsp.enable", "true")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
block()
|
block()
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ dependencies {
|
|||||||
api(libs.junitParams)
|
api(libs.junitParams)
|
||||||
api(projects.pklCommons) // for convenience
|
api(projects.pklCommons) // for convenience
|
||||||
implementation(libs.assertj)
|
implementation(libs.assertj)
|
||||||
runtimeOnly(projects.pklCerts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ object FileTestUtils {
|
|||||||
// drop some lines in the middle
|
// drop some lines in the middle
|
||||||
return dir.resolve("invalidCerts.pem").writeLines(lines.take(5) + lines.takeLast(5))
|
return dir.resolve("invalidCerts.pem").writeLines(lines.take(5) + lines.takeLast(5))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun writePklBuiltInCertificates(dir: Path): Path {
|
|
||||||
val text = javaClass.getResource("/org/pkl/certs/PklCARoots.pem")!!.readText()
|
|
||||||
return dir.resolve("PklCARoots.pem").apply { writeText(text) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Path.listFilesRecursively(): List<Path> =
|
fun Path.listFilesRecursively(): List<Path> =
|
||||||
|
|||||||
@@ -67,49 +67,22 @@ public interface HttpClient extends AutoCloseable {
|
|||||||
*
|
*
|
||||||
* <p>The given file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
* <p>The given file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
||||||
* certificates in PEM format.
|
* certificates in PEM format.
|
||||||
|
*
|
||||||
|
* <p>If no CA certificates are added via this method nor {@link #addCertificates(byte[])}, the
|
||||||
|
* built-in CA certificates of the Pkl native executable or JVM are used.
|
||||||
*/
|
*/
|
||||||
Builder addCertificates(Path file);
|
Builder addCertificates(Path path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a CA certificate file to the client's trust store.
|
* Adds CA certificate bytes to the client's trust store.
|
||||||
*
|
*
|
||||||
* <p>The given file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
* <p>The given cert must be an <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
||||||
* certificates in PEM format.
|
* certificate in PEM format.
|
||||||
*
|
*
|
||||||
* <p>This method is intended to be used for adding certificate files located on the class path.
|
* <p>If no CA certificates are added via this method nor {@link #addCertificates(Path)}, the
|
||||||
* To add certificate files located on the file system, use {@link #addCertificates(Path)}.
|
* built-in CA certificates of the Pkl native executable or JVM are used.
|
||||||
*
|
|
||||||
* @throws HttpClientInitException if the given URI has a scheme other than {@code jar:} or
|
|
||||||
* {@code file:}
|
|
||||||
*/
|
*/
|
||||||
Builder addCertificates(URI file);
|
Builder addCertificates(byte[] certificateBytes);
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the CA certificate files in {@code ~/.pkl/cacerts/} to the client's trust store.
|
|
||||||
*
|
|
||||||
* <p>Each file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
|
||||||
* certificates in PEM format. If {@code ~/.pkl/cacerts/} does not exist or is empty, Pkl's
|
|
||||||
* {@link #addBuiltInCertificates() built-in certificates} are added instead.
|
|
||||||
*
|
|
||||||
* <p>This method implements the default behavior of Pkl CLIs.
|
|
||||||
*
|
|
||||||
* <p>NOTE: This method requires the optional {@code pkl-certs} JAR to be present on the class
|
|
||||||
* path.
|
|
||||||
*
|
|
||||||
* @throws HttpClientInitException if an I/O error occurs while scanning {@code ~/.pkl/cacerts/}
|
|
||||||
* or the {@code pkl-certs} JAR is not found on the class path
|
|
||||||
*/
|
|
||||||
Builder addDefaultCliCertificates();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds Pkl's built-in CA certificates to the client's trust store.
|
|
||||||
*
|
|
||||||
* <p>NOTE: This method requires the optional {@code pkl-certs} JAR to be present on the class
|
|
||||||
* path.
|
|
||||||
*
|
|
||||||
* @throws HttpClientInitException if the {@code pkl-certs} JAR is not found on the class path
|
|
||||||
*/
|
|
||||||
Builder addBuiltInCertificates();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a test server's listening port.
|
* Sets a test server's listening port.
|
||||||
|
|||||||
@@ -15,11 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.pkl.core.http;
|
package org.pkl.core.http;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.ProxySelector;
|
import java.net.ProxySelector;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -27,27 +25,18 @@ import java.util.List;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import org.pkl.core.Release;
|
import org.pkl.core.Release;
|
||||||
import org.pkl.core.http.HttpClient.Builder;
|
import org.pkl.core.http.HttpClient.Builder;
|
||||||
import org.pkl.core.util.ErrorMessages;
|
|
||||||
import org.pkl.core.util.IoUtils;
|
|
||||||
|
|
||||||
final class HttpClientBuilder implements HttpClient.Builder {
|
final class HttpClientBuilder implements HttpClient.Builder {
|
||||||
private String userAgent;
|
private String userAgent;
|
||||||
private Duration connectTimeout = Duration.ofSeconds(60);
|
private Duration connectTimeout = Duration.ofSeconds(60);
|
||||||
private Duration requestTimeout = Duration.ofSeconds(60);
|
private Duration requestTimeout = Duration.ofSeconds(60);
|
||||||
private final Path caCertsDir;
|
|
||||||
private final List<Path> certificateFiles = new ArrayList<>();
|
private final List<Path> certificateFiles = new ArrayList<>();
|
||||||
private final List<URI> certificateUris = new ArrayList<>();
|
private final List<ByteBuffer> certificateBytes = new ArrayList<>();
|
||||||
private int testPort = -1;
|
private int testPort = -1;
|
||||||
private ProxySelector proxySelector;
|
private ProxySelector proxySelector;
|
||||||
|
|
||||||
HttpClientBuilder() {
|
HttpClientBuilder() {
|
||||||
this(IoUtils.getPklHomeDir().resolve("cacerts"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// only exists for testing
|
|
||||||
HttpClientBuilder(Path caCertsDir) {
|
|
||||||
var release = Release.current();
|
var release = Release.current();
|
||||||
this.caCertsDir = caCertsDir;
|
|
||||||
this.userAgent =
|
this.userAgent =
|
||||||
"Pkl/" + release.version() + " (" + release.os() + "; " + release.flavor() + ")";
|
"Pkl/" + release.version() + " (" + release.os() + "; " + release.flavor() + ")";
|
||||||
}
|
}
|
||||||
@@ -70,39 +59,14 @@ final class HttpClientBuilder implements HttpClient.Builder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpClient.Builder addCertificates(Path file) {
|
public HttpClient.Builder addCertificates(Path path) {
|
||||||
certificateFiles.add(file);
|
certificateFiles.add(path);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpClient.Builder addCertificates(URI url) {
|
public Builder addCertificates(byte[] certificateBytes) {
|
||||||
var scheme = url.getScheme();
|
this.certificateBytes.add(ByteBuffer.wrap(certificateBytes));
|
||||||
if (!"jar".equalsIgnoreCase(scheme) && !"file".equalsIgnoreCase(scheme)) {
|
|
||||||
throw new HttpClientInitException(ErrorMessages.create("expectedJarOrFileUrl", url));
|
|
||||||
}
|
|
||||||
certificateUris.add(url);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpClient.Builder addDefaultCliCertificates() {
|
|
||||||
var fileCount = certificateFiles.size();
|
|
||||||
if (Files.isDirectory(caCertsDir)) {
|
|
||||||
try (var files = Files.list(caCertsDir)) {
|
|
||||||
files.filter(Files::isRegularFile).forEach(certificateFiles::add);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new HttpClientInitException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (certificateFiles.size() == fileCount) {
|
|
||||||
addBuiltInCertificates();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HttpClient.Builder addBuiltInCertificates() {
|
|
||||||
certificateUris.add(getBuiltInCertificates());
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,27 +98,14 @@ final class HttpClientBuilder implements HttpClient.Builder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Supplier<HttpClient> doBuild() {
|
private Supplier<HttpClient> doBuild() {
|
||||||
// make defensive copies because Supplier may get called after builder was mutated
|
// make defensive copy because Supplier may get called after builder was mutated
|
||||||
var certificateFiles = List.copyOf(this.certificateFiles);
|
var certificateFiles = List.copyOf(this.certificateFiles);
|
||||||
var certificateUris = List.copyOf(this.certificateUris);
|
|
||||||
var proxySelector =
|
var proxySelector =
|
||||||
this.proxySelector != null ? this.proxySelector : java.net.ProxySelector.getDefault();
|
this.proxySelector != null ? this.proxySelector : java.net.ProxySelector.getDefault();
|
||||||
return () -> {
|
return () -> {
|
||||||
var jdkClient =
|
var jdkClient =
|
||||||
new JdkHttpClient(certificateFiles, certificateUris, connectTimeout, proxySelector);
|
new JdkHttpClient(certificateFiles, certificateBytes, connectTimeout, proxySelector);
|
||||||
return new RequestRewritingClient(userAgent, requestTimeout, testPort, jdkClient);
|
return new RequestRewritingClient(userAgent, requestTimeout, testPort, jdkClient);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static URI getBuiltInCertificates() {
|
|
||||||
var resource = HttpClientBuilder.class.getResource("/org/pkl/certs/PklCARoots.pem");
|
|
||||||
if (resource == null) {
|
|
||||||
throw new HttpClientInitException(ErrorMessages.create("cannotFindBuiltInCertificates"));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return resource.toURI();
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
throw new AssertionError("unreachable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
package org.pkl.core.http;
|
package org.pkl.core.http;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.lang.invoke.MethodHandle;
|
import java.lang.invoke.MethodHandle;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.lang.invoke.MethodType;
|
import java.lang.invoke.MethodType;
|
||||||
import java.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient.Redirect;
|
import java.net.http.HttpClient.Redirect;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.net.http.HttpResponse.BodyHandler;
|
import java.net.http.HttpResponse.BodyHandler;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.NoSuchFileException;
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -79,12 +80,12 @@ final class JdkHttpClient implements HttpClient {
|
|||||||
|
|
||||||
JdkHttpClient(
|
JdkHttpClient(
|
||||||
List<Path> certificateFiles,
|
List<Path> certificateFiles,
|
||||||
List<URI> certificateUris,
|
List<ByteBuffer> certificateBytes,
|
||||||
Duration connectTimeout,
|
Duration connectTimeout,
|
||||||
java.net.ProxySelector proxySelector) {
|
java.net.ProxySelector proxySelector) {
|
||||||
underlying =
|
underlying =
|
||||||
java.net.http.HttpClient.newBuilder()
|
java.net.http.HttpClient.newBuilder()
|
||||||
.sslContext(createSslContext(certificateFiles, certificateUris))
|
.sslContext(createSslContext(certificateFiles, certificateBytes))
|
||||||
.connectTimeout(connectTimeout)
|
.connectTimeout(connectTimeout)
|
||||||
.proxy(proxySelector)
|
.proxy(proxySelector)
|
||||||
.followRedirects(Redirect.NORMAL)
|
.followRedirects(Redirect.NORMAL)
|
||||||
@@ -126,10 +127,10 @@ final class JdkHttpClient implements HttpClient {
|
|||||||
|
|
||||||
// https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#security-algorithm-implementation-requirements
|
// https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#security-algorithm-implementation-requirements
|
||||||
private static SSLContext createSslContext(
|
private static SSLContext createSslContext(
|
||||||
List<Path> certificateFiles, List<URI> certificateUris) {
|
List<Path> certificateFiles, List<ByteBuffer> certificateBytes) {
|
||||||
try {
|
try {
|
||||||
if (certificateFiles.isEmpty() && certificateUris.isEmpty()) {
|
if (certificateFiles.isEmpty() && certificateBytes.isEmpty()) {
|
||||||
// fall back to JVM defaults (not Pkl built-in certs)
|
// use Pkl native executable's or JVM's built-in CA certificates
|
||||||
return SSLContext.getDefault();
|
return SSLContext.getDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +142,7 @@ final class JdkHttpClient implements HttpClient {
|
|||||||
|
|
||||||
var certFactory = CertificateFactory.getInstance("X.509");
|
var certFactory = CertificateFactory.getInstance("X.509");
|
||||||
Set<TrustAnchor> trustAnchors =
|
Set<TrustAnchor> trustAnchors =
|
||||||
createTrustAnchors(certFactory, certificateFiles, certificateUris);
|
createTrustAnchors(certFactory, certificateFiles, certificateBytes);
|
||||||
var pkixParameters = new PKIXBuilderParameters(trustAnchors, new X509CertSelector());
|
var pkixParameters = new PKIXBuilderParameters(trustAnchors, new X509CertSelector());
|
||||||
// equivalent of "com.sun.net.ssl.checkRevocation=true"
|
// equivalent of "com.sun.net.ssl.checkRevocation=true"
|
||||||
pkixParameters.setRevocationEnabled(true);
|
pkixParameters.setRevocationEnabled(true);
|
||||||
@@ -161,9 +162,8 @@ final class JdkHttpClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Set<TrustAnchor> createTrustAnchors(
|
private static Set<TrustAnchor> createTrustAnchors(
|
||||||
CertificateFactory factory, List<Path> certificateFiles, List<URI> certificateUris) {
|
CertificateFactory factory, List<Path> certificateFiles, List<ByteBuffer> certificateBytes) {
|
||||||
var anchors = new HashSet<TrustAnchor>();
|
var anchors = new HashSet<TrustAnchor>();
|
||||||
|
|
||||||
for (var file : certificateFiles) {
|
for (var file : certificateFiles) {
|
||||||
try (var stream = Files.newInputStream(file)) {
|
try (var stream = Files.newInputStream(file)) {
|
||||||
collectTrustAnchors(anchors, factory, stream, file);
|
collectTrustAnchors(anchors, factory, stream, file);
|
||||||
@@ -174,16 +174,10 @@ final class JdkHttpClient implements HttpClient {
|
|||||||
ErrorMessages.create("cannotReadCertFile", Exceptions.getRootReason(e)));
|
ErrorMessages.create("cannotReadCertFile", Exceptions.getRootReason(e)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (var byteBuffer : certificateBytes) {
|
||||||
for (var uri : certificateUris) {
|
var stream = new ByteArrayInputStream(byteBuffer.array());
|
||||||
try (var stream = uri.toURL().openStream()) {
|
collectTrustAnchors(anchors, factory, stream, "<unavailable>");
|
||||||
collectTrustAnchors(anchors, factory, stream, uri);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new HttpClientInitException(
|
|
||||||
ErrorMessages.create("cannotReadCertFile", Exceptions.getRootReason(e)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return anchors;
|
return anchors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ package org.pkl.core.service;
|
|||||||
|
|
||||||
import static org.pkl.core.module.ProjectDependenciesManager.PKL_PROJECT_FILENAME;
|
import static org.pkl.core.module.ProjectDependenciesManager.PKL_PROJECT_FILENAME;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -132,35 +131,35 @@ public final class ExecutorSpiImpl implements ExecutorSpi {
|
|||||||
|
|
||||||
private HttpClient getOrCreateHttpClient(ExecutorSpiOptions options) {
|
private HttpClient getOrCreateHttpClient(ExecutorSpiOptions options) {
|
||||||
List<Path> certificateFiles;
|
List<Path> certificateFiles;
|
||||||
List<URI> certificateUris;
|
List<byte[]> certificateBytes;
|
||||||
int testPort;
|
int testPort;
|
||||||
try {
|
try {
|
||||||
if (options instanceof ExecutorSpiOptions2 options2) {
|
if (options instanceof ExecutorSpiOptions2 options2) {
|
||||||
certificateFiles = options2.getCertificateFiles();
|
certificateFiles = options2.getCertificateFiles();
|
||||||
certificateUris = options2.getCertificateUris();
|
certificateBytes = options2.getCertificateBytes();
|
||||||
testPort = options2.getTestPort();
|
testPort = options2.getTestPort();
|
||||||
} else {
|
} else {
|
||||||
certificateFiles = List.of();
|
certificateFiles = List.of();
|
||||||
certificateUris = List.of();
|
certificateBytes = List.of();
|
||||||
testPort = -1;
|
testPort = -1;
|
||||||
}
|
}
|
||||||
// host pkl-executor does not have class ExecutorOptions2 defined.
|
// host pkl-executor does not have class ExecutorOptions2 defined.
|
||||||
// this will happen if the pkl-executor distribution is too old.
|
// this will happen if the pkl-executor distribution is too old.
|
||||||
} catch (NoClassDefFoundError e) {
|
} catch (NoClassDefFoundError e) {
|
||||||
certificateFiles = List.of();
|
certificateFiles = List.of();
|
||||||
certificateUris = List.of();
|
certificateBytes = List.of();
|
||||||
testPort = -1;
|
testPort = -1;
|
||||||
}
|
}
|
||||||
var clientKey = new HttpClientKey(certificateFiles, certificateUris, testPort);
|
var clientKey = new HttpClientKey(certificateFiles, certificateBytes, testPort);
|
||||||
return httpClients.computeIfAbsent(
|
return httpClients.computeIfAbsent(
|
||||||
clientKey,
|
clientKey,
|
||||||
(key) -> {
|
(key) -> {
|
||||||
var builder = HttpClient.builder();
|
var builder = HttpClient.builder();
|
||||||
for (var file : key.certificateFiles) {
|
for (var path : key.certificateFiles) {
|
||||||
builder.addCertificates(file);
|
builder.addCertificates(path);
|
||||||
}
|
}
|
||||||
for (var uri : key.certificateUris) {
|
for (var bytes : key.certificateBytes) {
|
||||||
builder.addCertificates(uri);
|
builder.addCertificates(bytes);
|
||||||
}
|
}
|
||||||
builder.setTestPort(key.testPort);
|
builder.setTestPort(key.testPort);
|
||||||
// If the above didn't add any certificates,
|
// If the above didn't add any certificates,
|
||||||
@@ -171,13 +170,13 @@ public final class ExecutorSpiImpl implements ExecutorSpi {
|
|||||||
|
|
||||||
private static final class HttpClientKey {
|
private static final class HttpClientKey {
|
||||||
final Set<Path> certificateFiles;
|
final Set<Path> certificateFiles;
|
||||||
final Set<URI> certificateUris;
|
final Set<byte[]> certificateBytes;
|
||||||
final int testPort;
|
final int testPort;
|
||||||
|
|
||||||
HttpClientKey(List<Path> certificateFiles, List<URI> certificateUris, int testPort) {
|
HttpClientKey(List<Path> certificateFiles, List<byte[]> certificateBytes, int testPort) {
|
||||||
// also serve as defensive copies
|
// also serves as defensive copy
|
||||||
this.certificateFiles = Set.copyOf(certificateFiles);
|
this.certificateFiles = Set.copyOf(certificateFiles);
|
||||||
this.certificateUris = Set.copyOf(certificateUris);
|
this.certificateBytes = Set.copyOf(certificateBytes);
|
||||||
this.testPort = testPort;
|
this.testPort = testPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,13 +190,13 @@ public final class ExecutorSpiImpl implements ExecutorSpi {
|
|||||||
}
|
}
|
||||||
HttpClientKey that = (HttpClientKey) obj;
|
HttpClientKey that = (HttpClientKey) obj;
|
||||||
return certificateFiles.equals(that.certificateFiles)
|
return certificateFiles.equals(that.certificateFiles)
|
||||||
&& certificateUris.equals(that.certificateUris)
|
&& certificateBytes.equals(that.certificateBytes)
|
||||||
&& testPort == that.testPort;
|
&& testPort == that.testPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(certificateFiles, certificateUris, testPort);
|
return Objects.hash(certificateFiles, certificateBytes, testPort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import java.net.http.HttpRequest
|
|||||||
import java.net.http.HttpResponse
|
import java.net.http.HttpResponse
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import kotlin.io.path.copyTo
|
|
||||||
import kotlin.io.path.createDirectories
|
|
||||||
import kotlin.io.path.createFile
|
import kotlin.io.path.createFile
|
||||||
|
import kotlin.io.path.readBytes
|
||||||
|
|
||||||
class HttpClientTest {
|
class HttpClientTest {
|
||||||
@Test
|
@Test
|
||||||
@@ -52,14 +51,21 @@ class HttpClientTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can load certificates from file system`() {
|
fun `can load certificates from regular file`() {
|
||||||
assertDoesNotThrow {
|
assertDoesNotThrow {
|
||||||
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate).build()
|
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `certificate file located on file system cannot be empty`(@TempDir tempDir: Path) {
|
fun `can load certificates from a byte array`() {
|
||||||
|
assertDoesNotThrow {
|
||||||
|
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate.readBytes()).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `certificate file cannot be empty`(@TempDir tempDir: Path) {
|
||||||
val file = tempDir.resolve("certs.pem").createFile()
|
val file = tempDir.resolve("certs.pem").createFile()
|
||||||
|
|
||||||
val e = assertThrows<HttpClientInitException> {
|
val e = assertThrows<HttpClientInitException> {
|
||||||
@@ -69,56 +75,10 @@ class HttpClientTest {
|
|||||||
assertThat(e).hasMessageContaining("empty")
|
assertThat(e).hasMessageContaining("empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `can load certificates from class path`() {
|
|
||||||
assertDoesNotThrow {
|
|
||||||
HttpClient.builder().addCertificates(javaClass.getResource("/org/pkl/certs/PklCARoots.pem")!!.toURI()).build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `only allows loading jar and file certificate URIs`() {
|
|
||||||
assertThrows<HttpClientInitException> {
|
|
||||||
HttpClient.builder().addCertificates(URI("https://example.com"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `certificate file located on class path cannot be empty`() {
|
|
||||||
val uri = javaClass.getResource("emptyCerts.pem")!!.toURI()
|
|
||||||
|
|
||||||
val e = assertThrows<HttpClientInitException> {
|
|
||||||
HttpClient.builder().addCertificates(uri).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(e).hasMessageContaining("empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can load built-in certificates`() {
|
fun `can load built-in certificates`() {
|
||||||
assertDoesNotThrow {
|
assertDoesNotThrow {
|
||||||
HttpClient.builder().addBuiltInCertificates().build()
|
HttpClient.builder().build()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `can load certificates from Pkl user home cacerts directory`(@TempDir tempDir: Path) {
|
|
||||||
val certsDir = tempDir.resolve(".pkl")
|
|
||||||
.resolve("cacerts")
|
|
||||||
.createDirectories()
|
|
||||||
.also { dir ->
|
|
||||||
FileTestUtils.selfSignedCertificate.copyTo(dir.resolve("certs.pem"))
|
|
||||||
}
|
|
||||||
|
|
||||||
assertDoesNotThrow {
|
|
||||||
HttpClientBuilder(certsDir).addDefaultCliCertificates().build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `loading certificates from cacerts directory falls back to built-in certificates`(@TempDir certsDir: Path) {
|
|
||||||
assertDoesNotThrow {
|
|
||||||
HttpClientBuilder(certsDir).addDefaultCliCertificates().build()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ package org.pkl.core.http
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertDoesNotThrow
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import org.junit.jupiter.api.io.TempDir
|
||||||
|
import org.pkl.commons.createTempFile
|
||||||
|
import org.pkl.commons.writeString
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.http.HttpRequest
|
import java.net.http.HttpRequest
|
||||||
import java.net.http.HttpResponse.BodyHandlers
|
import java.net.http.HttpResponse.BodyHandlers
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
class LazyHttpClientTest {
|
class LazyHttpClientTest {
|
||||||
@Test
|
@Test
|
||||||
fun `builds underlying client on first send`() {
|
fun `builds underlying client on first send`(@TempDir tempDir: Path) {
|
||||||
|
val certFile = tempDir.resolve("cert.pem").apply { writeString("broken") }
|
||||||
val client = HttpClient.builder()
|
val client = HttpClient.builder()
|
||||||
.addCertificates(javaClass.getResource("brokenCerts.pem")!!.toURI())
|
.addCertificates(certFile)
|
||||||
.buildLazily()
|
.buildLazily()
|
||||||
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
|
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
|
||||||
|
|
||||||
@@ -21,9 +26,10 @@ class LazyHttpClientTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `does not build underlying client unnecessarily`() {
|
fun `does not build underlying client unnecessarily`(@TempDir tempDir: Path) {
|
||||||
|
val certFile = tempDir.createTempFile().apply { writeString("broken") }
|
||||||
val client = HttpClient.builder()
|
val client = HttpClient.builder()
|
||||||
.addCertificates(javaClass.getResource("brokenCerts.pem")!!.toURI())
|
.addCertificates(certFile)
|
||||||
.buildLazily()
|
.buildLazily()
|
||||||
|
|
||||||
assertDoesNotThrow {
|
assertDoesNotThrow {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import org.pkl.core.SecurityManagers
|
|||||||
import org.pkl.core.packages.PackageResolver
|
import org.pkl.core.packages.PackageResolver
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
class ProjectDependenciesResolverTest {
|
class ProjectDependenciesResolverTest {
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
broken
|
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.pkl.executor;
|
package org.pkl.executor;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -52,7 +51,7 @@ public final class ExecutorOptions {
|
|||||||
|
|
||||||
private final List<Path> certificateFiles;
|
private final List<Path> certificateFiles;
|
||||||
|
|
||||||
private final List<URI> certificateUris;
|
private final List<byte[]> certificateBytes;
|
||||||
|
|
||||||
private final int testPort; // -1 means disabled
|
private final int testPort; // -1 means disabled
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ public final class ExecutorOptions {
|
|||||||
private /* @Nullable */ Path moduleCacheDir;
|
private /* @Nullable */ Path moduleCacheDir;
|
||||||
private /* @Nullable */ Path projectDir;
|
private /* @Nullable */ Path projectDir;
|
||||||
private List<Path> certificateFiles = List.of();
|
private List<Path> certificateFiles = List.of();
|
||||||
private List<URI> certificateUris = List.of();
|
private List<byte[]> certificateBytes = List.of();
|
||||||
private int testPort = -1; // -1 means disabled
|
private int testPort = -1; // -1 means disabled
|
||||||
private int spiOptionsVersion = -1; // -1 means use latest
|
private int spiOptionsVersion = -1; // -1 means use latest
|
||||||
|
|
||||||
@@ -188,15 +187,13 @@ public final class ExecutorOptions {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** API equivalent of the {@code --ca-certificates} CLI option. */
|
public Builder certificateBytes(List<byte[]> certificateBytes) {
|
||||||
public Builder certificateUris(List<URI> certificateUris) {
|
this.certificateBytes = certificateBytes;
|
||||||
this.certificateUris = certificateUris;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** API equivalent of the {@code --ca-certificates} CLI option. */
|
public Builder certificateBytes(byte[]... certificateBytes) {
|
||||||
public Builder certificateUris(URI... certificateUris) {
|
this.certificateBytes = List.of(certificateBytes);
|
||||||
this.certificateUris = List.of(certificateUris);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +222,7 @@ public final class ExecutorOptions {
|
|||||||
moduleCacheDir,
|
moduleCacheDir,
|
||||||
projectDir,
|
projectDir,
|
||||||
certificateFiles,
|
certificateFiles,
|
||||||
certificateUris,
|
certificateBytes,
|
||||||
testPort,
|
testPort,
|
||||||
spiOptionsVersion);
|
spiOptionsVersion);
|
||||||
}
|
}
|
||||||
@@ -290,7 +287,7 @@ public final class ExecutorOptions {
|
|||||||
/* @Nullable */ Path moduleCacheDir,
|
/* @Nullable */ Path moduleCacheDir,
|
||||||
/* @Nullable */ Path projectDir,
|
/* @Nullable */ Path projectDir,
|
||||||
List<Path> certificateFiles,
|
List<Path> certificateFiles,
|
||||||
List<URI> certificateUris,
|
List<byte[]> certificateBytes,
|
||||||
int testPort,
|
int testPort,
|
||||||
int spiOptionsVersion) {
|
int spiOptionsVersion) {
|
||||||
|
|
||||||
@@ -305,7 +302,7 @@ public final class ExecutorOptions {
|
|||||||
this.moduleCacheDir = moduleCacheDir;
|
this.moduleCacheDir = moduleCacheDir;
|
||||||
this.projectDir = projectDir;
|
this.projectDir = projectDir;
|
||||||
this.certificateFiles = List.copyOf(certificateFiles);
|
this.certificateFiles = List.copyOf(certificateFiles);
|
||||||
this.certificateUris = List.copyOf(certificateUris);
|
this.certificateBytes = List.copyOf(certificateBytes);
|
||||||
this.testPort = testPort;
|
this.testPort = testPort;
|
||||||
this.spiOptionsVersion = spiOptionsVersion;
|
this.spiOptionsVersion = spiOptionsVersion;
|
||||||
}
|
}
|
||||||
@@ -373,9 +370,8 @@ public final class ExecutorOptions {
|
|||||||
return certificateFiles;
|
return certificateFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** API equivalent of the {@code --ca-certificates} CLI option. */
|
public List<byte[]> getCertificateBytes() {
|
||||||
public List<URI> getCertificateUris() {
|
return certificateBytes;
|
||||||
return certificateUris;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -395,7 +391,7 @@ public final class ExecutorOptions {
|
|||||||
&& Objects.equals(moduleCacheDir, other.moduleCacheDir)
|
&& Objects.equals(moduleCacheDir, other.moduleCacheDir)
|
||||||
&& Objects.equals(projectDir, other.projectDir)
|
&& Objects.equals(projectDir, other.projectDir)
|
||||||
&& Objects.equals(certificateFiles, other.certificateFiles)
|
&& Objects.equals(certificateFiles, other.certificateFiles)
|
||||||
&& Objects.equals(certificateUris, other.certificateUris)
|
&& Objects.equals(certificateBytes, other.certificateBytes)
|
||||||
&& testPort == other.testPort
|
&& testPort == other.testPort
|
||||||
&& spiOptionsVersion == other.spiOptionsVersion;
|
&& spiOptionsVersion == other.spiOptionsVersion;
|
||||||
}
|
}
|
||||||
@@ -414,7 +410,7 @@ public final class ExecutorOptions {
|
|||||||
moduleCacheDir,
|
moduleCacheDir,
|
||||||
projectDir,
|
projectDir,
|
||||||
certificateFiles,
|
certificateFiles,
|
||||||
certificateUris,
|
certificateBytes,
|
||||||
testPort,
|
testPort,
|
||||||
spiOptionsVersion);
|
spiOptionsVersion);
|
||||||
}
|
}
|
||||||
@@ -444,8 +440,8 @@ public final class ExecutorOptions {
|
|||||||
+ projectDir
|
+ projectDir
|
||||||
+ ", certificateFiles="
|
+ ", certificateFiles="
|
||||||
+ certificateFiles
|
+ certificateFiles
|
||||||
+ ", certificateUris="
|
+ ", certificateBytes="
|
||||||
+ certificateUris
|
+ certificateBytes
|
||||||
+ ", testPort="
|
+ ", testPort="
|
||||||
+ testPort
|
+ testPort
|
||||||
+ ", spiOptionsVersion="
|
+ ", spiOptionsVersion="
|
||||||
@@ -468,7 +464,7 @@ public final class ExecutorOptions {
|
|||||||
moduleCacheDir,
|
moduleCacheDir,
|
||||||
projectDir,
|
projectDir,
|
||||||
certificateFiles,
|
certificateFiles,
|
||||||
certificateUris,
|
certificateBytes,
|
||||||
testPort);
|
testPort);
|
||||||
case 1 -> // for testing only
|
case 1 -> // for testing only
|
||||||
new ExecutorSpiOptions(
|
new ExecutorSpiOptions(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.pkl.executor.spi.v1;
|
package org.pkl.executor.spi.v1;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -24,7 +23,7 @@ import java.util.Map;
|
|||||||
public class ExecutorSpiOptions2 extends ExecutorSpiOptions {
|
public class ExecutorSpiOptions2 extends ExecutorSpiOptions {
|
||||||
private final List<Path> certificateFiles;
|
private final List<Path> certificateFiles;
|
||||||
|
|
||||||
private final List<URI> certificateUris;
|
private final List<byte[]> certificateBytes;
|
||||||
|
|
||||||
private final int testPort;
|
private final int testPort;
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ public class ExecutorSpiOptions2 extends ExecutorSpiOptions {
|
|||||||
Path moduleCacheDir,
|
Path moduleCacheDir,
|
||||||
Path projectDir,
|
Path projectDir,
|
||||||
List<Path> certificateFiles,
|
List<Path> certificateFiles,
|
||||||
List<URI> certificateUris,
|
List<byte[]> certificateBytes,
|
||||||
int testPort) {
|
int testPort) {
|
||||||
super(
|
super(
|
||||||
allowedModules,
|
allowedModules,
|
||||||
@@ -54,7 +53,7 @@ public class ExecutorSpiOptions2 extends ExecutorSpiOptions {
|
|||||||
moduleCacheDir,
|
moduleCacheDir,
|
||||||
projectDir);
|
projectDir);
|
||||||
this.certificateFiles = certificateFiles;
|
this.certificateFiles = certificateFiles;
|
||||||
this.certificateUris = certificateUris;
|
this.certificateBytes = certificateBytes;
|
||||||
this.testPort = testPort;
|
this.testPort = testPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +61,8 @@ public class ExecutorSpiOptions2 extends ExecutorSpiOptions {
|
|||||||
return certificateFiles;
|
return certificateFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<URI> getCertificateUris() {
|
public List<byte[]> getCertificateBytes() {
|
||||||
return certificateUris;
|
return certificateBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getTestPort() {
|
public int getTestPort() {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import java.nio.file.Path
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.*
|
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.Proxy
|
||||||
import org.pkl.core.module.PathElement
|
import org.pkl.core.module.PathElement
|
||||||
import org.pkl.core.packages.Checksums
|
import org.pkl.core.packages.Checksums
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ data class CreateEvaluatorRequest(
|
|||||||
val cacheDir: Path?,
|
val cacheDir: Path?,
|
||||||
val outputFormat: String?,
|
val outputFormat: String?,
|
||||||
val project: Project?,
|
val project: Project?,
|
||||||
val http: Http?,
|
val http: Http?
|
||||||
) : ClientRequestMessage() {
|
) : ClientRequestMessage() {
|
||||||
override val type = MessageType.CREATE_EVALUATOR_REQUEST
|
override val type = MessageType.CREATE_EVALUATOR_REQUEST
|
||||||
|
|
||||||
@@ -151,7 +151,8 @@ data class CreateEvaluatorRequest(
|
|||||||
rootDir.equalsNullable(other.rootDir) &&
|
rootDir.equalsNullable(other.rootDir) &&
|
||||||
cacheDir.equalsNullable(other.cacheDir) &&
|
cacheDir.equalsNullable(other.cacheDir) &&
|
||||||
outputFormat.equalsNullable(other.outputFormat) &&
|
outputFormat.equalsNullable(other.outputFormat) &&
|
||||||
project.equalsNullable(other.project)
|
project.equalsNullable(other.project) &&
|
||||||
|
http.equalsNullable(other.http)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DuplicatedCode") // false duplicate within method
|
@Suppress("DuplicatedCode") // false duplicate within method
|
||||||
@@ -170,6 +171,31 @@ data class CreateEvaluatorRequest(
|
|||||||
result = 31 * result + outputFormat.hashCode()
|
result = 31 * result + outputFormat.hashCode()
|
||||||
result = 31 * result + project.hashCode()
|
result = 31 * result + project.hashCode()
|
||||||
result = 31 * result + type.hashCode()
|
result = 31 * result + type.hashCode()
|
||||||
|
result = 31 * result + http.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Http(
|
||||||
|
/** PEM-format CA certificates as raw bytes. */
|
||||||
|
val caCertificates: ByteArray?,
|
||||||
|
/** Proxy settings */
|
||||||
|
val proxy: Proxy?
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Http) return false
|
||||||
|
|
||||||
|
if (caCertificates != null) {
|
||||||
|
if (other.caCertificates == null) return false
|
||||||
|
if (!caCertificates.contentEquals(other.caCertificates)) return false
|
||||||
|
} else if (other.caCertificates != null) return false
|
||||||
|
return Objects.equals(proxy, other.proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = caCertificates?.contentHashCode() ?: 0
|
||||||
|
result = 31 * result + (proxy?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,10 +255,11 @@ internal class MessagePackDecoder(private val unpacker: MessageUnpacker) : Messa
|
|||||||
return Project(projectFileUri, null, dependencies)
|
return Project(projectFileUri, null, dependencies)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Map<Value, Value>.unpackHttp(): PklEvaluatorSettings.Http? {
|
private fun Map<Value, Value>.unpackHttp(): Http? {
|
||||||
val httpMap = getNullable("http")?.asMapValue()?.map() ?: return null
|
val httpMap = getNullable("http")?.asMapValue()?.map() ?: return null
|
||||||
val proxy = httpMap.unpackProxy()
|
val proxy = httpMap.unpackProxy()
|
||||||
return PklEvaluatorSettings.Http(proxy)
|
val caCertificates = httpMap.getNullable("caCertificates")?.asBinaryValue()?.asByteArray()
|
||||||
|
return Http(caCertificates, proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Map<Value, Value>.unpackProxy(): PklEvaluatorSettings.Proxy? {
|
private fun Map<Value, Value>.unpackProxy(): PklEvaluatorSettings.Proxy? {
|
||||||
|
|||||||
@@ -49,6 +49,21 @@ internal class MessagePackEncoder(private val packer: MessagePacker) : MessageEn
|
|||||||
packDependencies(project.dependencies)
|
packDependencies(project.dependencies)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MessagePacker.packHttp(http: Http) {
|
||||||
|
if ((http.caCertificates ?: http.proxy) == null) {
|
||||||
|
packMapHeader(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
packMapHeader(0, http.caCertificates, http.proxy)
|
||||||
|
packKeyValue("caCertificates", http.caCertificates)
|
||||||
|
http.proxy?.let { proxy ->
|
||||||
|
packString("proxy")
|
||||||
|
packMapHeader(0, proxy.address, proxy.noProxy)
|
||||||
|
packKeyValue("address", proxy.address?.toString())
|
||||||
|
packKeyValue("noProxy", proxy.noProxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun MessagePacker.packDependencies(dependencies: Map<String, Dependency>) {
|
private fun MessagePacker.packDependencies(dependencies: Map<String, Dependency>) {
|
||||||
packMapHeader(dependencies.size)
|
packMapHeader(dependencies.size)
|
||||||
for ((name, dep) in dependencies) {
|
for ((name, dep) in dependencies) {
|
||||||
@@ -87,7 +102,15 @@ internal class MessagePackEncoder(private val packer: MessagePacker) : MessageEn
|
|||||||
when (msg.type.code) {
|
when (msg.type.code) {
|
||||||
MessageType.CREATE_EVALUATOR_REQUEST.code -> {
|
MessageType.CREATE_EVALUATOR_REQUEST.code -> {
|
||||||
msg as CreateEvaluatorRequest
|
msg as CreateEvaluatorRequest
|
||||||
packMapHeader(8, msg.timeout, msg.rootDir, msg.cacheDir, msg.outputFormat, msg.project)
|
packMapHeader(
|
||||||
|
8,
|
||||||
|
msg.timeout,
|
||||||
|
msg.rootDir,
|
||||||
|
msg.cacheDir,
|
||||||
|
msg.outputFormat,
|
||||||
|
msg.project,
|
||||||
|
msg.http
|
||||||
|
)
|
||||||
packKeyValue("requestId", msg.requestId)
|
packKeyValue("requestId", msg.requestId)
|
||||||
packKeyValue("allowedModules", msg.allowedModules?.map { it.toString() })
|
packKeyValue("allowedModules", msg.allowedModules?.map { it.toString() })
|
||||||
packKeyValue("allowedResources", msg.allowedResources?.map { it.toString() })
|
packKeyValue("allowedResources", msg.allowedResources?.map { it.toString() })
|
||||||
@@ -116,6 +139,10 @@ internal class MessagePackEncoder(private val packer: MessagePacker) : MessageEn
|
|||||||
packString("project")
|
packString("project")
|
||||||
packProject(msg.project)
|
packProject(msg.project)
|
||||||
}
|
}
|
||||||
|
if (msg.http != null) {
|
||||||
|
packString("http")
|
||||||
|
packHttp(msg.http)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MessageType.CREATE_EVALUATOR_RESPONSE.code -> {
|
MessageType.CREATE_EVALUATOR_RESPONSE.code -> {
|
||||||
msg as CreateEvaluatorResponse
|
msg as CreateEvaluatorResponse
|
||||||
@@ -243,7 +270,8 @@ internal class MessagePackEncoder(private val packer: MessagePacker) : MessageEn
|
|||||||
value2: Any?,
|
value2: Any?,
|
||||||
value3: Any?,
|
value3: Any?,
|
||||||
value4: Any?,
|
value4: Any?,
|
||||||
value5: Any?
|
value5: Any?,
|
||||||
|
value6: Any?
|
||||||
) =
|
) =
|
||||||
packMapHeader(
|
packMapHeader(
|
||||||
size +
|
size +
|
||||||
@@ -251,7 +279,8 @@ internal class MessagePackEncoder(private val packer: MessagePacker) : MessageEn
|
|||||||
(if (value2 != null) 1 else 0) +
|
(if (value2 != null) 1 else 0) +
|
||||||
(if (value3 != null) 1 else 0) +
|
(if (value3 != null) 1 else 0) +
|
||||||
(if (value4 != null) 1 else 0) +
|
(if (value4 != null) 1 else 0) +
|
||||||
(if (value5 != null) 1 else 0)
|
(if (value5 != null) 1 else 0) +
|
||||||
|
(if (value6 != null) 1 else 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun MessagePacker.packKeyValue(name: String, value: Int?) {
|
private fun MessagePacker.packKeyValue(name: String, value: Int?) {
|
||||||
|
|||||||
@@ -162,12 +162,13 @@ class Server(private val transport: MessageTransport) : AutoCloseable {
|
|||||||
val properties = message.properties ?: emptyMap()
|
val properties = message.properties ?: emptyMap()
|
||||||
val timeout = message.timeout
|
val timeout = message.timeout
|
||||||
val cacheDir = message.cacheDir
|
val cacheDir = message.cacheDir
|
||||||
val http =
|
val httpClient =
|
||||||
with(HttpClient.builder()) {
|
with(HttpClient.builder()) {
|
||||||
message.http?.proxy?.let { proxy ->
|
message.http?.proxy?.let { proxy ->
|
||||||
setProxy(proxy.address, message.http.proxy?.noProxy ?: listOf())
|
setProxy(proxy.address, proxy.noProxy ?: listOf())
|
||||||
proxy.address?.let(IoUtils::setSystemProxy)
|
proxy.address?.let(IoUtils::setSystemProxy)
|
||||||
}
|
}
|
||||||
|
message.http?.caCertificates?.let { caCertificates -> addCertificates(caCertificates) }
|
||||||
buildLazily()
|
buildLazily()
|
||||||
}
|
}
|
||||||
val dependencies =
|
val dependencies =
|
||||||
@@ -183,7 +184,7 @@ class Server(private val transport: MessageTransport) : AutoCloseable {
|
|||||||
SecurityManagers.defaultTrustLevels,
|
SecurityManagers.defaultTrustLevels,
|
||||||
rootDir
|
rootDir
|
||||||
),
|
),
|
||||||
http,
|
httpClient,
|
||||||
ClientLogger(evaluatorId, transport),
|
ClientLogger(evaluatorId, transport),
|
||||||
createModuleKeyFactories(message, evaluatorId, resolver),
|
createModuleKeyFactories(message, evaluatorId, resolver),
|
||||||
createResourceReaders(message, evaluatorId, resolver),
|
createResourceReaders(message, evaluatorId, resolver),
|
||||||
|
|||||||
@@ -1013,7 +1013,8 @@ abstract class AbstractServerTest {
|
|||||||
moduleReaders: List<ModuleReaderSpec> = listOf(),
|
moduleReaders: List<ModuleReaderSpec> = listOf(),
|
||||||
modulePaths: List<Path> = listOf(),
|
modulePaths: List<Path> = listOf(),
|
||||||
project: Project? = null,
|
project: Project? = null,
|
||||||
cacheDir: Path? = null
|
cacheDir: Path? = null,
|
||||||
|
http: Http? = null,
|
||||||
): Long {
|
): Long {
|
||||||
val message =
|
val message =
|
||||||
CreateEvaluatorRequest(
|
CreateEvaluatorRequest(
|
||||||
@@ -1030,7 +1031,7 @@ abstract class AbstractServerTest {
|
|||||||
cacheDir = cacheDir,
|
cacheDir = cacheDir,
|
||||||
outputFormat = null,
|
outputFormat = null,
|
||||||
project = project,
|
project = project,
|
||||||
http = null,
|
http = http
|
||||||
)
|
)
|
||||||
|
|
||||||
send(message)
|
send(message)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.assertj.core.api.Assertions.assertThat
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import org.msgpack.core.MessagePack
|
import org.msgpack.core.MessagePack
|
||||||
|
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
|
||||||
import org.pkl.core.module.PathElement
|
import org.pkl.core.module.PathElement
|
||||||
import org.pkl.core.packages.Checksums
|
import org.pkl.core.packages.Checksums
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ class MessagePackCodecTest {
|
|||||||
isGlobbable = false,
|
isGlobbable = false,
|
||||||
isLocal = false
|
isLocal = false
|
||||||
)
|
)
|
||||||
|
@Suppress("HttpUrlsUsage")
|
||||||
roundtrip(
|
roundtrip(
|
||||||
CreateEvaluatorRequest(
|
CreateEvaluatorRequest(
|
||||||
requestId = 123,
|
requestId = 123,
|
||||||
@@ -113,7 +115,11 @@ class MessagePackCodecTest {
|
|||||||
RemoteDependency(URI("package://localhost:0/baz@1.1.0"), Checksums("abc123"))
|
RemoteDependency(URI("package://localhost:0/baz@1.1.0"), Checksums("abc123"))
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
http = null,
|
http =
|
||||||
|
Http(
|
||||||
|
proxy = PklEvaluatorSettings.Proxy(URI("http://foo.com:1234"), listOf("bar", "baz")),
|
||||||
|
caCertificates = byteArrayOf(1, 2, 3, 4)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ include("bench")
|
|||||||
include("docs")
|
include("docs")
|
||||||
include("stdlib")
|
include("stdlib")
|
||||||
|
|
||||||
include("pkl-certs")
|
|
||||||
include("pkl-cli")
|
include("pkl-cli")
|
||||||
include("pkl-codegen-java")
|
include("pkl-codegen-java")
|
||||||
include("pkl-codegen-kotlin")
|
include("pkl-codegen-kotlin")
|
||||||
|
|||||||
Reference in New Issue
Block a user