mirror of
https://github.com/apple/pkl.git
synced 2026-04-23 08:48:36 +02:00
Use java.net.http.HttpClient instead of java.net.Http(s)URLConnection (#217)
Moving to java.net.http.HttpClient brings many benefits, including HTTP/2 support and the ability to make asynchronous requests. Major additions and changes: - Introduce a lightweight org.pkl.core.http.HttpClient API. This keeps some flexibility and allows to enforce behavior such as setting the User-Agent header. - Provide an implementation that delegates to java.net.http.HttpClient. - Use HttpClient for all HTTP(s) requests across the codebase. This required adding an HttpClient parameter to constructors and factory methods of multiple classes, some of which are public APIs. - Manage CA certificates per HTTP client instead of per JVM. This makes it unnecessary to set JVM-wide system/security properties and default SSLSocketFactory's. - Add executor v2 options to the executor SPI - Add pkl-certs as a new artifact, and remove certs from pkl-commons-cli artifact Each HTTP client maintains its own connection pool and SSLContext. For efficiency reasons, It's best to reuse clients whenever feasible. To avoid memory leaks, clients are not stored in static fields. HTTP clients are expensive to create. For this reason, EvaluatorBuilder defaults to a "lazy" client that creates the underlying java.net.http.HttpClient on the first send (which may never happen).
This commit is contained in:
@@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import org.openjdk.jmh.annotations.*;
|
import org.openjdk.jmh.annotations.*;
|
||||||
import org.openjdk.jmh.util.TempFile;
|
import org.openjdk.jmh.util.TempFile;
|
||||||
import org.openjdk.jmh.util.TempFileManager;
|
import org.openjdk.jmh.util.TempFileManager;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.ModuleKeyFactories;
|
import org.pkl.core.module.ModuleKeyFactories;
|
||||||
import org.pkl.core.repl.ReplRequest;
|
import org.pkl.core.repl.ReplRequest;
|
||||||
import org.pkl.core.repl.ReplResponse;
|
import org.pkl.core.repl.ReplResponse;
|
||||||
@@ -39,6 +40,7 @@ public class ListSort {
|
|||||||
private static final ReplServer repl =
|
private static final ReplServer repl =
|
||||||
new ReplServer(
|
new ReplServer(
|
||||||
SecurityManagers.defaultManager,
|
SecurityManagers.defaultManager,
|
||||||
|
HttpClient.dummyClient(),
|
||||||
Loggers.stdErr(),
|
Loggers.stdErr(),
|
||||||
List.of(ModuleKeyFactories.standardLibrary),
|
List.of(ModuleKeyFactories.standardLibrary),
|
||||||
List.of(ResourceReaders.file()),
|
List.of(ResourceReaders.file()),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.pkl.core.repl.ReplServer
|
|||||||
import org.pkl.core.resource.ResourceReaders
|
import org.pkl.core.resource.ResourceReaders
|
||||||
import org.pkl.core.util.IoUtils
|
import org.pkl.core.util.IoUtils
|
||||||
import org.antlr.v4.runtime.ParserRuleContext
|
import org.antlr.v4.runtime.ParserRuleContext
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import kotlin.io.path.isDirectory
|
import kotlin.io.path.isDirectory
|
||||||
import kotlin.io.path.isRegularFile
|
import kotlin.io.path.isRegularFile
|
||||||
@@ -78,6 +79,7 @@ class DocSnippetTestsEngine : HierarchicalTestEngine<DocSnippetTestsEngine.Execu
|
|||||||
override fun createExecutionContext(request: ExecutionRequest): ExecutionContext {
|
override fun createExecutionContext(request: ExecutionRequest): ExecutionContext {
|
||||||
val replServer = ReplServer(
|
val replServer = ReplServer(
|
||||||
SecurityManagers.defaultManager,
|
SecurityManagers.defaultManager,
|
||||||
|
HttpClient.dummyClient(),
|
||||||
Loggers.stdErr(),
|
Loggers.stdErr(),
|
||||||
listOf(
|
listOf(
|
||||||
ModuleKeyFactories.standardLibrary,
|
ModuleKeyFactories.standardLibrary,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ msgpack = { group = "org.msgpack", name = "msgpack-core", version.ref = "msgpack
|
|||||||
nuValidator = { group = "nu.validator", name = "validator", version.ref = "nuValidator" }
|
nuValidator = { group = "nu.validator", name = "validator", version.ref = "nuValidator" }
|
||||||
# to be replaced with https://github.com/usethesource/capsule or https://github.com/lacuna/bifurcan
|
# to be replaced with https://github.com/usethesource/capsule or https://github.com/lacuna/bifurcan
|
||||||
paguro = { group = "org.organicdesign", name = "Paguro", version.ref = "paguro" }
|
paguro = { group = "org.organicdesign", name = "Paguro", version.ref = "paguro" }
|
||||||
|
pklConfigJavaAll025 = { group = "org.pkl-lang", name = "pkl-config-java-all", version = "0.25.0" }
|
||||||
shadowPlugin = { group = "gradle.plugin.com.github.johnrengelman", name = "shadow", version.ref = "shadowPlugin" }
|
shadowPlugin = { group = "gradle.plugin.com.github.johnrengelman", name = "shadow", version.ref = "shadowPlugin" }
|
||||||
slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||||
slf4jSimple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
|
slf4jSimple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
|
||||||
|
|||||||
19
pkl-certs/pkl-certs.gradle.kts
Normal file
19
pkl-certs/pkl-certs.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -156,7 +156,7 @@ fun Exec.configureExecutable(isEnabled: Boolean, outputFile: File, extraArgs: Li
|
|||||||
,"--no-fallback"
|
,"--no-fallback"
|
||||||
,"-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/commons/cli/commands/IncludedCARoots.pem"
|
,"-H:IncludeResources=org/pkl/certs/PklCARoots.pem"
|
||||||
//,"-H:IncludeResources=org/pkl/core/Release.properties"
|
//,"-H:IncludeResources=org/pkl/core/Release.properties"
|
||||||
,"-H:IncludeResourceBundles=org.pkl.core.errorMessages"
|
,"-H:IncludeResourceBundles=org.pkl.core.errorMessages"
|
||||||
,"--macro:truffle"
|
,"--macro:truffle"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class CliPackageDownloader(
|
|||||||
if (moduleCacheDir == null) {
|
if (moduleCacheDir == null) {
|
||||||
throw CliException("Cannot download packages because no cache directory is specified.")
|
throw CliException("Cannot download packages because no cache directory is specified.")
|
||||||
}
|
}
|
||||||
val packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir)
|
val packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir)
|
||||||
val errors = mutableMapOf<PackageUri, Throwable>()
|
val errors = mutableMapOf<PackageUri, Throwable>()
|
||||||
for (pkg in packageUris) {
|
for (pkg in packageUris) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class CliProjectPackager(
|
|||||||
outputPath,
|
outputPath,
|
||||||
stackFrameTransformer,
|
stackFrameTransformer,
|
||||||
securityManager,
|
securityManager,
|
||||||
|
httpClient,
|
||||||
skipPublishCheck,
|
skipPublishCheck,
|
||||||
consoleWriter
|
consoleWriter
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class CliProjectResolver(
|
|||||||
SecurityManagers.defaultTrustLevels,
|
SecurityManagers.defaultTrustLevels,
|
||||||
rootDir
|
rootDir
|
||||||
),
|
),
|
||||||
|
httpClient,
|
||||||
moduleCacheDir
|
moduleCacheDir
|
||||||
)
|
)
|
||||||
val dependencies = ProjectDependenciesResolver(project, packageResolver, errWriter).resolve()
|
val dependencies = ProjectDependenciesResolver(project, packageResolver, errWriter).resolve()
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ internal class CliRepl(private val options: CliEvaluatorOptions) : CliCommand(op
|
|||||||
SecurityManagers.defaultTrustLevels,
|
SecurityManagers.defaultTrustLevels,
|
||||||
rootDir
|
rootDir
|
||||||
),
|
),
|
||||||
|
httpClient,
|
||||||
Loggers.stdErr(),
|
Loggers.stdErr(),
|
||||||
listOf(
|
listOf(
|
||||||
ModuleKeyFactories.standardLibrary,
|
ModuleKeyFactories.standardLibrary,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import org.pkl.server.Server
|
|||||||
class CliServer(options: CliBaseOptions) : CliCommand(options) {
|
class CliServer(options: CliBaseOptions) : CliCommand(options) {
|
||||||
override fun doRun() =
|
override fun doRun() =
|
||||||
try {
|
try {
|
||||||
val server = Server(MessageTransports.stream(System.`in`, System.out))
|
val server = Server(MessageTransports.stream(System.`in`, System.out), httpClient)
|
||||||
server.use { it.start() }
|
server.use { it.start() }
|
||||||
} catch (e: ProtocolException) {
|
} catch (e: ProtocolException) {
|
||||||
throw CliException(e.message!!)
|
throw CliException(e.message!!)
|
||||||
|
|||||||
@@ -21,19 +21,18 @@ import java.net.URI
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import kotlin.io.path.createDirectories
|
import kotlin.io.path.*
|
||||||
import kotlin.io.path.listDirectoryEntries
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatCode
|
import org.assertj.core.api.Assertions.assertThatCode
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
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.junit.jupiter.api.io.TempDir
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.EnumSource
|
import org.junit.jupiter.params.provider.EnumSource
|
||||||
import org.pkl.commons.*
|
import org.pkl.commons.*
|
||||||
import org.pkl.commons.cli.CliBaseOptions
|
import org.pkl.commons.cli.CliBaseOptions
|
||||||
import org.pkl.commons.cli.CliException
|
import org.pkl.commons.cli.CliException
|
||||||
import org.pkl.commons.cli.commands.BaseOptions
|
|
||||||
import org.pkl.commons.test.FileTestUtils
|
import org.pkl.commons.test.FileTestUtils
|
||||||
import org.pkl.commons.test.PackageServer
|
import org.pkl.commons.test.PackageServer
|
||||||
import org.pkl.core.OutputFormat
|
import org.pkl.core.OutputFormat
|
||||||
@@ -1158,8 +1157,46 @@ result = someLib.x
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `not including the self signed certificate will result in a error`() {
|
fun `gives decent error message if certificate file contains random text`() {
|
||||||
|
val certsFile = tempDir.writeFile("random.pem", "RANDOM")
|
||||||
|
val err = assertThrows<CliException> { evalModuleThatImportsPackage(certsFile) }
|
||||||
|
assertThat(err)
|
||||||
|
.hasMessageContaining("Error parsing CA certificate file `${certsFile.pathString}`:")
|
||||||
|
.hasMessageContaining("No certificate data found")
|
||||||
|
.hasMessageNotContainingAny("java.", "sun.") // class names have been filtered out
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `gives decent error message if certificate file is emtpy`(@TempDir tempDir: Path) {
|
||||||
|
val emptyCerts = tempDir.writeEmptyFile("empty.pem")
|
||||||
|
val err = assertThrows<CliException> { evalModuleThatImportsPackage(emptyCerts) }
|
||||||
|
assertThat(err).hasMessageContaining("CA certificate file `${emptyCerts.pathString}` is empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `gives decent error message if certificate cannot be parsed`(@TempDir tempDir: Path) {
|
||||||
|
val invalidCerts = FileTestUtils.writeCertificateWithMissingLines(tempDir)
|
||||||
|
val err = assertThrows<CliException> { evalModuleThatImportsPackage(invalidCerts) }
|
||||||
|
assertThat(err)
|
||||||
|
// no assert for detail message because it differs between JDK implementations
|
||||||
|
.hasMessageContaining("Error parsing CA certificate file `${invalidCerts.pathString}`:")
|
||||||
|
.hasMessageNotContainingAny("java.", "sun.") // class names have been filtered out
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `gives decent error message if CLI doesn't have the required CA certificate`() {
|
||||||
PackageServer.ensureStarted()
|
PackageServer.ensureStarted()
|
||||||
|
// provide SOME certs to prevent CliEvaluator from falling back to ~/.pkl/cacerts
|
||||||
|
val builtInCerts = FileTestUtils.writePklBuiltInCertificates(tempDir)
|
||||||
|
val err = assertThrows<CliException> { evalModuleThatImportsPackage(builtInCerts) }
|
||||||
|
assertThat(err)
|
||||||
|
// on some JDK11's this doesn't cause SSLHandshakeException but some other SSLException
|
||||||
|
// .hasMessageContaining("Error during SSL handshake with host `localhost`:")
|
||||||
|
.hasMessageContaining("unable to find valid certification path to requested target")
|
||||||
|
.hasMessageNotContainingAny("java.", "sun.") // class names have been filtered out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evalModuleThatImportsPackage(certsFile: Path) {
|
||||||
val moduleUri =
|
val moduleUri =
|
||||||
writePklFile(
|
writePklFile(
|
||||||
"test.pkl",
|
"test.pkl",
|
||||||
@@ -1168,22 +1205,17 @@ result = someLib.x
|
|||||||
|
|
||||||
res = Swallow
|
res = Swallow
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
|
||||||
)
|
)
|
||||||
val buffer = StringWriter()
|
|
||||||
val options =
|
val options =
|
||||||
CliEvaluatorOptions(
|
CliEvaluatorOptions(
|
||||||
CliBaseOptions(
|
CliBaseOptions(
|
||||||
sourceModules = listOf(moduleUri),
|
sourceModules = listOf(moduleUri),
|
||||||
workingDir = tempDir,
|
caCertificates = listOf(certsFile),
|
||||||
moduleCacheDir = tempDir,
|
noCache = true
|
||||||
noCache = true,
|
|
||||||
// ensure we override any previously set root cert to the default buundle.
|
|
||||||
caCertificates = listOf(BaseOptions.Companion.includedCARootCerts())
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val err = assertThrows<CliException> { CliEvaluator(options, consoleWriter = buffer).run() }
|
CliEvaluator(options).run()
|
||||||
assertThat(err.message).contains("unable to find valid certification path to requested target")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun writePklFile(fileName: String, contents: String = defaultContents): URI {
|
private fun writePklFile(fileName: String, contents: String = defaultContents): URI {
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class CliPackageDownloaderTest {
|
|||||||
|
|
||||||
Failed to download package://bogus.domain/notAPackage@1.0.0 because:
|
Failed to download package://bogus.domain/notAPackage@1.0.0 because:
|
||||||
Exception when making request `GET https://bogus.domain/notAPackage@1.0.0`:
|
Exception when making request `GET https://bogus.domain/notAPackage@1.0.0`:
|
||||||
bogus.domain
|
Error connecting to host `bogus.domain`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
.trimIndent()
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import org.pkl.commons.readString
|
|||||||
import org.pkl.commons.test.FileTestUtils
|
import org.pkl.commons.test.FileTestUtils
|
||||||
import org.pkl.commons.test.PackageServer
|
import org.pkl.commons.test.PackageServer
|
||||||
import org.pkl.commons.writeString
|
import org.pkl.commons.writeString
|
||||||
import org.pkl.core.runtime.CertificateUtils
|
|
||||||
|
|
||||||
class CliProjectPackagerTest {
|
class CliProjectPackagerTest {
|
||||||
@Test
|
@Test
|
||||||
@@ -868,7 +867,6 @@ class CliProjectPackagerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `publish checks`(@TempDir tempDir: Path) {
|
fun `publish checks`(@TempDir tempDir: Path) {
|
||||||
PackageServer.ensureStarted()
|
PackageServer.ensureStarted()
|
||||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
|
||||||
tempDir.writeFile("project/main.pkl", "res = 1")
|
tempDir.writeFile("project/main.pkl", "res = 1")
|
||||||
tempDir.writeFile(
|
tempDir.writeFile(
|
||||||
"project/PklProject",
|
"project/PklProject",
|
||||||
@@ -888,7 +886,10 @@ class CliProjectPackagerTest {
|
|||||||
val e =
|
val e =
|
||||||
assertThrows<CliException> {
|
assertThrows<CliException> {
|
||||||
CliProjectPackager(
|
CliProjectPackager(
|
||||||
CliBaseOptions(workingDir = tempDir),
|
CliBaseOptions(
|
||||||
|
workingDir = tempDir,
|
||||||
|
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
|
||||||
|
),
|
||||||
listOf(tempDir.resolve("project")),
|
listOf(tempDir.resolve("project")),
|
||||||
CliTestOptions(),
|
CliTestOptions(),
|
||||||
".out/%{name}@%{version}",
|
".out/%{name}@%{version}",
|
||||||
@@ -912,7 +913,6 @@ class CliProjectPackagerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `publish check when package is not yet published`(@TempDir tempDir: Path) {
|
fun `publish check when package is not yet published`(@TempDir tempDir: Path) {
|
||||||
PackageServer.ensureStarted()
|
PackageServer.ensureStarted()
|
||||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
|
||||||
tempDir.writeFile("project/main.pkl", "res = 1")
|
tempDir.writeFile("project/main.pkl", "res = 1")
|
||||||
tempDir.writeFile(
|
tempDir.writeFile(
|
||||||
"project/PklProject",
|
"project/PklProject",
|
||||||
@@ -930,7 +930,10 @@ class CliProjectPackagerTest {
|
|||||||
)
|
)
|
||||||
val out = StringWriter()
|
val out = StringWriter()
|
||||||
CliProjectPackager(
|
CliProjectPackager(
|
||||||
CliBaseOptions(workingDir = tempDir),
|
CliBaseOptions(
|
||||||
|
workingDir = tempDir,
|
||||||
|
caCertificates = listOf(FileTestUtils.selfSignedCertificate)
|
||||||
|
),
|
||||||
listOf(tempDir.resolve("project")),
|
listOf(tempDir.resolve("project")),
|
||||||
CliTestOptions(),
|
CliTestOptions(),
|
||||||
".out/%{name}@%{version}",
|
".out/%{name}@%{version}",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.pkl.commons.toPath
|
|||||||
import org.pkl.core.Loggers
|
import org.pkl.core.Loggers
|
||||||
import org.pkl.core.SecurityManagers
|
import org.pkl.core.SecurityManagers
|
||||||
import org.pkl.core.StackFrameTransformers
|
import org.pkl.core.StackFrameTransformers
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.module.ModuleKeyFactories
|
import org.pkl.core.module.ModuleKeyFactories
|
||||||
import org.pkl.core.repl.ReplRequest
|
import org.pkl.core.repl.ReplRequest
|
||||||
import org.pkl.core.repl.ReplResponse
|
import org.pkl.core.repl.ReplResponse
|
||||||
@@ -30,6 +31,7 @@ class ReplMessagesTest {
|
|||||||
private val server =
|
private val server =
|
||||||
ReplServer(
|
ReplServer(
|
||||||
SecurityManagers.defaultManager,
|
SecurityManagers.defaultManager,
|
||||||
|
HttpClient.dummyClient(),
|
||||||
Loggers.stdErr(),
|
Loggers.stdErr(),
|
||||||
listOf(ModuleKeyFactories.standardLibrary),
|
listOf(ModuleKeyFactories.standardLibrary),
|
||||||
listOf(),
|
listOf(),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(projects.pklCommons)
|
implementation(projects.pklCommons)
|
||||||
testImplementation(projects.pklCommonsTest)
|
testImplementation(projects.pklCommonsTest)
|
||||||
|
runtimeOnly(projects.pklCerts)
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.module.ProjectDependenciesManager
|
import org.pkl.core.module.ProjectDependenciesManager
|
||||||
import org.pkl.core.util.IoUtils
|
import org.pkl.core.util.IoUtils
|
||||||
|
|
||||||
@@ -113,15 +114,15 @@ data class CliBaseOptions(
|
|||||||
val testMode: Boolean = false,
|
val testMode: Boolean = false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [X.509 certificates](https://en.wikipedia.org/wiki/X.509) in PEM format.
|
* The CA certificates to trust.
|
||||||
*
|
*
|
||||||
* Elements can either be a [Path] or a [java.io.InputStream]. Input streams are closed when
|
* The given files must contain [X.509](https://en.wikipedia.org/wiki/X.509) certificates in PEM
|
||||||
* [CliCommand] is initialized.
|
* format.
|
||||||
*
|
*
|
||||||
* If not empty, this determines the CA root certs used for all HTTPS requests. Warning: this
|
* If [caCertificates] is the empty list, the certificate files in `~/.pkl/cacerts/` are used. If
|
||||||
* affects the whole Java runtime, not just the Pkl API!
|
* `~/.pkl/cacerts/` does not exist or is empty, Pkl's built-in CA certificates are used.
|
||||||
*/
|
*/
|
||||||
val caCertificates: List<Any> = emptyList(),
|
val caCertificates: List<Path> = listOf(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -167,4 +168,26 @@ data class CliBaseOptions(
|
|||||||
projectDir?.resolve(ProjectDependenciesManager.PKL_PROJECT_FILENAME)
|
projectDir?.resolve(ProjectDependenciesManager.PKL_PROJECT_FILENAME)
|
||||||
?: normalizedWorkingDir.getProjectFile(rootDir)
|
?: normalizedWorkingDir.getProjectFile(rootDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** [caCertificates] after normalization. */
|
||||||
|
val normalizedCaCertificates: List<Path> = caCertificates.map(normalizedWorkingDir::resolve)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP client shared between CLI commands created with this [CliBaseOptions] instance.
|
||||||
|
*
|
||||||
|
* To release the resources held by the HTTP client in a timely manner, call its `close()` method.
|
||||||
|
*/
|
||||||
|
val httpClient: HttpClient by lazy {
|
||||||
|
with(HttpClient.builder()) {
|
||||||
|
if (normalizedCaCertificates.isEmpty()) {
|
||||||
|
addDefaultCliCertificates()
|
||||||
|
} else {
|
||||||
|
for (file in normalizedCaCertificates) addCertificates(file)
|
||||||
|
}
|
||||||
|
// Lazy building significantly reduces execution time of commands that do minimal work.
|
||||||
|
// However, it means that HTTP client initialization errors won't surface until an HTTP
|
||||||
|
// request is made.
|
||||||
|
buildLazily()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,24 +18,18 @@ package org.pkl.commons.cli
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import org.pkl.core.*
|
import org.pkl.core.*
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.module.ModuleKeyFactories
|
import org.pkl.core.module.ModuleKeyFactories
|
||||||
import org.pkl.core.module.ModuleKeyFactory
|
import org.pkl.core.module.ModuleKeyFactory
|
||||||
import org.pkl.core.module.ModulePathResolver
|
import org.pkl.core.module.ModulePathResolver
|
||||||
import org.pkl.core.project.Project
|
import org.pkl.core.project.Project
|
||||||
import org.pkl.core.resource.ResourceReader
|
import org.pkl.core.resource.ResourceReader
|
||||||
import org.pkl.core.resource.ResourceReaders
|
import org.pkl.core.resource.ResourceReaders
|
||||||
import org.pkl.core.runtime.CertificateUtils
|
|
||||||
import org.pkl.core.settings.PklSettings
|
import org.pkl.core.settings.PklSettings
|
||||||
import org.pkl.core.util.IoUtils
|
import org.pkl.core.util.IoUtils
|
||||||
|
|
||||||
/** Building block for CLI commands. Configured programmatically to allow for embedding. */
|
/** Building block for CLI commands. Configured programmatically to allow for embedding. */
|
||||||
abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
||||||
init {
|
|
||||||
if (cliOptions.caCertificates.isNotEmpty()) {
|
|
||||||
CertificateUtils.setupAllX509CertificatesGlobally(cliOptions.caCertificates)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Runs this command. */
|
/** Runs this command. */
|
||||||
fun run() {
|
fun run() {
|
||||||
if (cliOptions.testMode) {
|
if (cliOptions.testMode) {
|
||||||
@@ -158,6 +152,10 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// share HTTP client with other commands with the same cliOptions
|
||||||
|
protected val httpClient: HttpClient
|
||||||
|
get() = cliOptions.httpClient
|
||||||
|
|
||||||
protected fun moduleKeyFactories(modulePathResolver: ModulePathResolver): List<ModuleKeyFactory> {
|
protected fun moduleKeyFactories(modulePathResolver: ModulePathResolver): List<ModuleKeyFactory> {
|
||||||
return buildList {
|
return buildList {
|
||||||
add(ModuleKeyFactories.standardLibrary)
|
add(ModuleKeyFactories.standardLibrary)
|
||||||
@@ -195,6 +193,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
|||||||
.setStackFrameTransformer(stackFrameTransformer)
|
.setStackFrameTransformer(stackFrameTransformer)
|
||||||
.apply { project?.let { setProjectDependencies(it.dependencies) } }
|
.apply { project?.let { setProjectDependencies(it.dependencies) } }
|
||||||
.setSecurityManager(securityManager)
|
.setSecurityManager(securityManager)
|
||||||
|
.setHttpClient(httpClient)
|
||||||
.setExternalProperties(externalProperties)
|
.setExternalProperties(externalProperties)
|
||||||
.setEnvironmentVariables(environmentVariables)
|
.setEnvironmentVariables(environmentVariables)
|
||||||
.addModuleKeyFactories(moduleKeyFactories(modulePathResolver))
|
.addModuleKeyFactories(moduleKeyFactories(modulePathResolver))
|
||||||
|
|||||||
@@ -20,27 +20,15 @@ import com.github.ajalt.clikt.parameters.options.*
|
|||||||
import com.github.ajalt.clikt.parameters.types.long
|
import com.github.ajalt.clikt.parameters.types.long
|
||||||
import com.github.ajalt.clikt.parameters.types.path
|
import com.github.ajalt.clikt.parameters.types.path
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
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.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import java.util.stream.Collectors
|
|
||||||
import kotlin.io.path.exists
|
|
||||||
import kotlin.io.path.isDirectory
|
|
||||||
import kotlin.io.path.isRegularFile
|
|
||||||
import org.pkl.commons.cli.CliBaseOptions
|
import org.pkl.commons.cli.CliBaseOptions
|
||||||
import org.pkl.core.util.IoUtils
|
import org.pkl.core.util.IoUtils
|
||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
class BaseOptions : OptionGroup() {
|
class BaseOptions : OptionGroup() {
|
||||||
companion object {
|
|
||||||
fun includedCARootCerts(): InputStream {
|
|
||||||
return BaseOptions::class.java.getResourceAsStream("IncludedCARoots.pem")!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val defaults = CliBaseOptions()
|
private val defaults = CliBaseOptions()
|
||||||
|
|
||||||
private val output =
|
private val output =
|
||||||
@@ -142,28 +130,11 @@ class BaseOptions : OptionGroup() {
|
|||||||
option(
|
option(
|
||||||
names = arrayOf("--ca-certificates"),
|
names = arrayOf("--ca-certificates"),
|
||||||
metavar = "<path>",
|
metavar = "<path>",
|
||||||
help = "Replaces the built-in CA certificates with the provided certificate file."
|
help = "Only trust CA certificates from the provided file(s)."
|
||||||
)
|
)
|
||||||
.path()
|
.path()
|
||||||
.multiple()
|
.multiple()
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. If `--ca-certificates` option is not empty, use that.
|
|
||||||
* 2. If directory `~/.pkl/cacerts` is not empty, use that.
|
|
||||||
* 3. Use the bundled CA certificates.
|
|
||||||
*/
|
|
||||||
private fun getEffectiveCaCertificates(): List<Any> {
|
|
||||||
return caCertificates
|
|
||||||
.ifEmpty {
|
|
||||||
val home = System.getProperty("user.home")
|
|
||||||
val cacerts = Path.of(home, ".pkl", "cacerts")
|
|
||||||
if (cacerts.exists() && cacerts.isDirectory())
|
|
||||||
Files.list(cacerts).filter(Path::isRegularFile).collect(Collectors.toList())
|
|
||||||
else emptyList()
|
|
||||||
}
|
|
||||||
.ifEmpty { listOf(includedCARootCerts()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun baseOptions(
|
fun baseOptions(
|
||||||
modules: List<URI>,
|
modules: List<URI>,
|
||||||
projectOptions: ProjectOptions? = null,
|
projectOptions: ProjectOptions? = null,
|
||||||
@@ -186,7 +157,7 @@ class BaseOptions : OptionGroup() {
|
|||||||
testMode = testMode,
|
testMode = testMode,
|
||||||
omitProjectSettings = projectOptions?.omitProjectSettings ?: false,
|
omitProjectSettings = projectOptions?.omitProjectSettings ?: false,
|
||||||
noProject = projectOptions?.noProject ?: false,
|
noProject = projectOptions?.noProject ?: false,
|
||||||
caCertificates = getEffectiveCaCertificates()
|
caCertificates = caCertificates
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.commons.test
|
||||||
|
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpClient
|
||||||
|
import java.net.http.HttpHeaders
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
import java.util.*
|
||||||
|
import javax.net.ssl.SSLSession
|
||||||
|
|
||||||
|
class FakeHttpResponse<T : Any> : HttpResponse<T> {
|
||||||
|
companion object {
|
||||||
|
fun <T : Any> withBody(block: FakeHttpResponse<T>.() -> Unit): FakeHttpResponse<T> =
|
||||||
|
FakeHttpResponse<T>().apply(block)
|
||||||
|
|
||||||
|
fun withoutBody(block: FakeHttpResponse<Unit>.() -> Unit): FakeHttpResponse<Unit> =
|
||||||
|
FakeHttpResponse<Unit>().apply { body = Unit }.apply(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusCode: Int = 200
|
||||||
|
var request: HttpRequest = HttpRequest.newBuilder().uri(URI("https://example.com")).build()
|
||||||
|
var uri: URI = URI("https://example.com")
|
||||||
|
var version: HttpClient.Version = HttpClient.Version.HTTP_2
|
||||||
|
|
||||||
|
lateinit var headers: HttpHeaders
|
||||||
|
lateinit var body: T
|
||||||
|
|
||||||
|
override fun statusCode(): Int = statusCode
|
||||||
|
|
||||||
|
override fun request(): HttpRequest = request
|
||||||
|
|
||||||
|
override fun previousResponse(): Optional<HttpResponse<T>> = Optional.empty()
|
||||||
|
|
||||||
|
override fun headers(): HttpHeaders = headers
|
||||||
|
|
||||||
|
override fun body(): T = body
|
||||||
|
|
||||||
|
override fun sslSession(): Optional<SSLSession> = Optional.empty()
|
||||||
|
|
||||||
|
override fun uri(): URI = uri
|
||||||
|
|
||||||
|
override fun version(): HttpClient.Version = version
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
package org.pkl.commons.test
|
package org.pkl.commons.test
|
||||||
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import java.util.stream.Collectors
|
||||||
import kotlin.io.path.*
|
import kotlin.io.path.*
|
||||||
import kotlin.streams.toList
|
|
||||||
import org.assertj.core.api.Assertions.fail
|
import org.assertj.core.api.Assertions.fail
|
||||||
import org.pkl.commons.*
|
import org.pkl.commons.*
|
||||||
|
|
||||||
@@ -32,10 +32,23 @@ object FileTestUtils {
|
|||||||
val selfSignedCertificate: Path by lazy {
|
val selfSignedCertificate: Path by lazy {
|
||||||
rootProjectDir.resolve("pkl-commons-test/build/keystore/localhost.pem")
|
rootProjectDir.resolve("pkl-commons-test/build/keystore/localhost.pem")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun writeCertificateWithMissingLines(dir: Path): Path {
|
||||||
|
val lines = selfSignedCertificate.readLines()
|
||||||
|
// drop some lines in the middle
|
||||||
|
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> =
|
||||||
walk(99).use { paths -> paths.filter { it.isRegularFile() || it.isSymbolicLink() }.toList() }
|
walk(99).use { paths ->
|
||||||
|
paths.filter { it.isRegularFile() || it.isSymbolicLink() }.collect(Collectors.toList())
|
||||||
|
}
|
||||||
|
|
||||||
data class SnippetOutcome(val expectedOutFile: Path, val actual: String, val success: Boolean) {
|
data class SnippetOutcome(val expectedOutFile: Path, val actual: String, val success: Boolean) {
|
||||||
private val expectedErrFile =
|
private val expectedErrFile =
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.commons.test
|
||||||
|
|
||||||
|
class FilteringClassLoader(parent: ClassLoader, private val includeFilter: (String) -> Boolean) :
|
||||||
|
ClassLoader(parent) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
registerAsParallelCapable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadClass(name: String, resolve: Boolean): Class<*> {
|
||||||
|
if (!includeFilter(name)) throw ClassNotFoundException(name)
|
||||||
|
return super.loadClass(name, resolve)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ dependencies {
|
|||||||
implementation(libs.snakeYaml)
|
implementation(libs.snakeYaml)
|
||||||
|
|
||||||
testImplementation(projects.pklCommonsTest)
|
testImplementation(projects.pklCommonsTest)
|
||||||
|
|
||||||
add("generatorImplementation", libs.javaPoet)
|
add("generatorImplementation", libs.javaPoet)
|
||||||
add("generatorImplementation", libs.truffleApi)
|
add("generatorImplementation", libs.truffleApi)
|
||||||
add("generatorImplementation", libs.kotlinStdLib)
|
add("generatorImplementation", libs.kotlinStdLib)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import org.pkl.core.SecurityManagers.StandardBuilder;
|
import org.pkl.core.SecurityManagers.StandardBuilder;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.ModuleKeyFactories;
|
import org.pkl.core.module.ModuleKeyFactories;
|
||||||
import org.pkl.core.module.ModuleKeyFactory;
|
import org.pkl.core.module.ModuleKeyFactory;
|
||||||
import org.pkl.core.module.ModulePathResolver;
|
import org.pkl.core.module.ModulePathResolver;
|
||||||
@@ -38,6 +39,10 @@ public final class EvaluatorBuilder {
|
|||||||
|
|
||||||
private @Nullable SecurityManager securityManager;
|
private @Nullable SecurityManager securityManager;
|
||||||
|
|
||||||
|
// Default to a client with a fixed set of built-in certificates.
|
||||||
|
// Make it lazy to avoid creating a client unnecessarily.
|
||||||
|
private HttpClient httpClient = HttpClient.builder().buildLazily();
|
||||||
|
|
||||||
private Logger logger = Loggers.noop();
|
private Logger logger = Loggers.noop();
|
||||||
|
|
||||||
private final List<ModuleKeyFactory> moduleKeyFactories = new ArrayList<>();
|
private final List<ModuleKeyFactory> moduleKeyFactories = new ArrayList<>();
|
||||||
@@ -226,6 +231,21 @@ public final class EvaluatorBuilder {
|
|||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the HTTP client to be used.
|
||||||
|
*
|
||||||
|
* <p>Defaults to {@code HttpClient.builder().buildLazily()}.
|
||||||
|
*/
|
||||||
|
public EvaluatorBuilder setHttpClient(HttpClient httpClient) {
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the currently set HTTP client. */
|
||||||
|
public HttpClient getHttpClient() {
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given module key factory. Factories will be asked to resolve module keys in the order
|
* Adds the given module key factory. Factories will be asked to resolve module keys in the order
|
||||||
* they have been added to this builder.
|
* they have been added to this builder.
|
||||||
@@ -468,6 +488,7 @@ public final class EvaluatorBuilder {
|
|||||||
return new EvaluatorImpl(
|
return new EvaluatorImpl(
|
||||||
stackFrameTransformer,
|
stackFrameTransformer,
|
||||||
securityManager,
|
securityManager,
|
||||||
|
httpClient,
|
||||||
new LoggerImpl(logger, stackFrameTransformer),
|
new LoggerImpl(logger, stackFrameTransformer),
|
||||||
// copy to shield against subsequent modification through builder
|
// copy to shield against subsequent modification through builder
|
||||||
new ArrayList<>(moduleKeyFactories),
|
new ArrayList<>(moduleKeyFactories),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import java.util.function.Supplier;
|
|||||||
import org.graalvm.polyglot.Context;
|
import org.graalvm.polyglot.Context;
|
||||||
import org.pkl.core.ast.ConstantValueNode;
|
import org.pkl.core.ast.ConstantValueNode;
|
||||||
import org.pkl.core.ast.internal.ToStringNodeGen;
|
import org.pkl.core.ast.internal.ToStringNodeGen;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.ModuleKeyFactory;
|
import org.pkl.core.module.ModuleKeyFactory;
|
||||||
import org.pkl.core.module.ProjectDependenciesManager;
|
import org.pkl.core.module.ProjectDependenciesManager;
|
||||||
import org.pkl.core.packages.PackageResolver;
|
import org.pkl.core.packages.PackageResolver;
|
||||||
@@ -69,6 +70,7 @@ public class EvaluatorImpl implements Evaluator {
|
|||||||
public EvaluatorImpl(
|
public EvaluatorImpl(
|
||||||
StackFrameTransformer transformer,
|
StackFrameTransformer transformer,
|
||||||
SecurityManager manager,
|
SecurityManager manager,
|
||||||
|
HttpClient httpClient,
|
||||||
Logger logger,
|
Logger logger,
|
||||||
Collection<ModuleKeyFactory> factories,
|
Collection<ModuleKeyFactory> factories,
|
||||||
Collection<ResourceReader> readers,
|
Collection<ResourceReader> readers,
|
||||||
@@ -83,7 +85,7 @@ public class EvaluatorImpl implements Evaluator {
|
|||||||
frameTransformer = transformer;
|
frameTransformer = transformer;
|
||||||
moduleResolver = new ModuleResolver(factories);
|
moduleResolver = new ModuleResolver(factories);
|
||||||
this.logger = new BufferedLogger(logger);
|
this.logger = new BufferedLogger(logger);
|
||||||
packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir);
|
packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir);
|
||||||
polyglotContext =
|
polyglotContext =
|
||||||
VmUtils.createContext(
|
VmUtils.createContext(
|
||||||
() -> {
|
() -> {
|
||||||
@@ -92,6 +94,7 @@ public class EvaluatorImpl implements Evaluator {
|
|||||||
new VmContext.Holder(
|
new VmContext.Holder(
|
||||||
transformer,
|
transformer,
|
||||||
manager,
|
manager,
|
||||||
|
httpClient,
|
||||||
moduleResolver,
|
moduleResolver,
|
||||||
new ResourceManager(manager, readers),
|
new ResourceManager(manager, readers),
|
||||||
this.logger,
|
this.logger,
|
||||||
|
|||||||
@@ -23,4 +23,8 @@ public class PklException extends RuntimeException {
|
|||||||
public PklException(String message) {
|
public PklException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PklException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import org.pkl.core.SecurityManagerException;
|
|||||||
import org.pkl.core.ast.VmModifier;
|
import org.pkl.core.ast.VmModifier;
|
||||||
import org.pkl.core.ast.member.ObjectMember;
|
import org.pkl.core.ast.member.ObjectMember;
|
||||||
import org.pkl.core.ast.member.UntypedObjectMemberNode;
|
import org.pkl.core.ast.member.UntypedObjectMemberNode;
|
||||||
|
import org.pkl.core.http.HttpClientInitException;
|
||||||
import org.pkl.core.module.ResolvedModuleKey;
|
import org.pkl.core.module.ResolvedModuleKey;
|
||||||
import org.pkl.core.packages.PackageLoadError;
|
import org.pkl.core.packages.PackageLoadError;
|
||||||
import org.pkl.core.runtime.BaseModule;
|
import org.pkl.core.runtime.BaseModule;
|
||||||
@@ -113,7 +114,7 @@ public class ImportGlobNode extends AbstractImportNode {
|
|||||||
frame.materialize(), BaseModule.getMappingClass().getPrototype(), members);
|
frame.materialize(), BaseModule.getMappingClass().getPrototype(), members);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw exceptionBuilder().evalError("ioErrorResolvingGlob", importUri).withCause(e).build();
|
throw exceptionBuilder().evalError("ioErrorResolvingGlob", importUri).withCause(e).build();
|
||||||
} catch (SecurityManagerException e) {
|
} catch (SecurityManagerException | HttpClientInitException e) {
|
||||||
throw exceptionBuilder().withCause(e).build();
|
throw exceptionBuilder().withCause(e).build();
|
||||||
} catch (PackageLoadError e) {
|
} catch (PackageLoadError e) {
|
||||||
throw exceptionBuilder().adhocEvalError(e.getMessage()).build();
|
throw exceptionBuilder().adhocEvalError(e.getMessage()).build();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.oracle.truffle.api.nodes.NodeInfo;
|
|||||||
import com.oracle.truffle.api.source.SourceSection;
|
import com.oracle.truffle.api.source.SourceSection;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import org.pkl.core.SecurityManagerException;
|
import org.pkl.core.SecurityManagerException;
|
||||||
|
import org.pkl.core.http.HttpClientInitException;
|
||||||
import org.pkl.core.module.ResolvedModuleKey;
|
import org.pkl.core.module.ResolvedModuleKey;
|
||||||
import org.pkl.core.packages.PackageLoadError;
|
import org.pkl.core.packages.PackageLoadError;
|
||||||
import org.pkl.core.runtime.VmContext;
|
import org.pkl.core.runtime.VmContext;
|
||||||
@@ -60,7 +61,7 @@ public final class ImportNode extends AbstractImportNode {
|
|||||||
context.getSecurityManager().checkImportModule(currentModule.getUri(), importUri);
|
context.getSecurityManager().checkImportModule(currentModule.getUri(), importUri);
|
||||||
var moduleToImport = context.getModuleResolver().resolve(importUri, this);
|
var moduleToImport = context.getModuleResolver().resolve(importUri, this);
|
||||||
importedModule = language.loadModule(moduleToImport, this);
|
importedModule = language.loadModule(moduleToImport, this);
|
||||||
} catch (SecurityManagerException | PackageLoadError e) {
|
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
|
||||||
throw exceptionBuilder().withCause(e).build();
|
throw exceptionBuilder().withCause(e).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.core.http;
|
||||||
|
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.net.http.HttpResponse.BodyHandler;
|
||||||
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
|
|
||||||
|
/** An {@code HttpClient} implementation that throws {@code AssertionError} on every send. */
|
||||||
|
@ThreadSafe
|
||||||
|
final class DummyHttpClient implements HttpClient {
|
||||||
|
@Override
|
||||||
|
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler) {
|
||||||
|
throw new AssertionError("Dummy HTTP client cannot send request: " + request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {}
|
||||||
|
}
|
||||||
182
pkl-core/src/main/java/org/pkl/core/http/HttpClient.java
Normal file
182
pkl-core/src/main/java/org/pkl/core/http/HttpClient.java
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.core.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.net.http.HttpTimeoutException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An HTTP client.
|
||||||
|
*
|
||||||
|
* <p>To create a new HTTP client, use a {@linkplain #builder() builder}. To send {@linkplain
|
||||||
|
* HttpRequest requests} and retrieve their {@linkplain HttpResponse responses}, use {@link #send}.
|
||||||
|
* To release resources held by the client, use {@link #close}.
|
||||||
|
*
|
||||||
|
* <p>HTTP clients are thread-safe. Each client maintains its own connection pool and {@link
|
||||||
|
* SSLContext}. For efficiency reasons, clients should be reused whenever possible.
|
||||||
|
*/
|
||||||
|
public interface HttpClient extends AutoCloseable {
|
||||||
|
|
||||||
|
/** A builder of {@linkplain HttpClient HTTP clients}. */
|
||||||
|
interface Builder {
|
||||||
|
/**
|
||||||
|
* Sets the {@code User-Agent} header.
|
||||||
|
*
|
||||||
|
* <p>Defaults to {@code "Pkl/$version ($os; $flavor)"}.
|
||||||
|
*/
|
||||||
|
Builder setUserAgent(String userAgent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the timeout for connecting to a server.
|
||||||
|
*
|
||||||
|
* <p>Defaults to 60 seconds.
|
||||||
|
*/
|
||||||
|
Builder setConnectTimeout(java.time.Duration timeout);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the timeout for the interval between sending a request and receiving response headers.
|
||||||
|
*
|
||||||
|
* <p>Defaults to 60 seconds. To set a timeout for a specific request, use {@link
|
||||||
|
* HttpRequest.Builder#timeout}.
|
||||||
|
*/
|
||||||
|
Builder setRequestTimeout(java.time.Duration timeout);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a CA certificate file to the client's trust store.
|
||||||
|
*
|
||||||
|
* <p>The given file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
||||||
|
* certificates in PEM format.
|
||||||
|
*/
|
||||||
|
Builder addCertificates(Path file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a CA certificate file to the client's trust store.
|
||||||
|
*
|
||||||
|
* <p>The given file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
||||||
|
* certificates in PEM format.
|
||||||
|
*
|
||||||
|
* <p>This method is intended to be used for adding certificate files located on the class path.
|
||||||
|
* To add certificate files located on the file system, use {@link #addCertificates(Path)}.
|
||||||
|
*
|
||||||
|
* @throws HttpClientInitException if the given URI has a scheme other than {@code jar:} or
|
||||||
|
* {@code file:}
|
||||||
|
*/
|
||||||
|
Builder addCertificates(URI file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code HttpClient} from the current state of this builder.
|
||||||
|
*
|
||||||
|
* @throws HttpClientInitException if an error occurs while initializing the client
|
||||||
|
*/
|
||||||
|
HttpClient build();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an {@code HTTPClient} wrapper that defers building the actual HTTP client until the
|
||||||
|
* wrapper's {@link HttpClient#send} method is called.
|
||||||
|
*
|
||||||
|
* <p>Note: When using this method, any exception thrown when building the actual HTTP client is
|
||||||
|
* equally deferred.
|
||||||
|
*/
|
||||||
|
HttpClient buildLazily();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code HTTPClient} builder with default settings.
|
||||||
|
*
|
||||||
|
* <p>The default settings are:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Connect timeout: 60 seconds
|
||||||
|
* <li>Request timeout: 60 seconds
|
||||||
|
* <li>CA certificates: none (falls back to the JVM's {@linkplain SSLContext#getDefault()
|
||||||
|
* default SSL context})
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
static Builder builder() {
|
||||||
|
return new HttpClientBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a client that throws {@link AssertionError} on every attempt to send a request. */
|
||||||
|
static HttpClient dummyClient() {
|
||||||
|
return new DummyHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an HTTP request. The response body is processed by the given body handler.
|
||||||
|
*
|
||||||
|
* <p>If the request does not specify a {@linkplain HttpRequest#timeout timeout}, the client's
|
||||||
|
* {@linkplain Builder#setRequestTimeout request timeout} is used. If the request does not specify
|
||||||
|
* a preferred {@linkplain HttpRequest#version() HTTP version}, HTTP/2 is used. The request's
|
||||||
|
* {@code User-Agent} header is set to the client's {@link Builder#setUserAgent User-Agent}
|
||||||
|
* header.
|
||||||
|
*
|
||||||
|
* <p>Depending on the given body handler, this method blocks until response headers or the entire
|
||||||
|
* response body has been received. If response headers are not received within the request
|
||||||
|
* timeout, {@link HttpTimeoutException} is thrown.
|
||||||
|
*
|
||||||
|
* <p>For additional information on how to use this method, see {@link
|
||||||
|
* java.net.http.HttpClient#send}.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs when sending or receiving
|
||||||
|
* @throws HttpClientInitException if an error occurs while initializing a {@linkplain
|
||||||
|
* Builder#buildLazily lazy} client
|
||||||
|
*/
|
||||||
|
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes this client.
|
||||||
|
*
|
||||||
|
* <p>This method makes a best effort to release the resources held by this client in a timely
|
||||||
|
* manner. This may involve waiting for pending requests to complete.
|
||||||
|
*
|
||||||
|
* <p>Subsequent calls to this method have no effect. Subsequent calls to any other method throw
|
||||||
|
* {@link IllegalStateException}.
|
||||||
|
*/
|
||||||
|
void close();
|
||||||
|
}
|
||||||
135
pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java
Normal file
135
pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.core.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import org.pkl.core.Release;
|
||||||
|
import org.pkl.core.util.ErrorMessages;
|
||||||
|
|
||||||
|
final class HttpClientBuilder implements HttpClient.Builder {
|
||||||
|
private final Path userHome;
|
||||||
|
private String userAgent;
|
||||||
|
private Duration connectTimeout = Duration.ofSeconds(60);
|
||||||
|
private Duration requestTimeout = Duration.ofSeconds(60);
|
||||||
|
private final List<Path> certificateFiles = new ArrayList<>();
|
||||||
|
private final List<URI> certificateUris = new ArrayList<>();
|
||||||
|
|
||||||
|
HttpClientBuilder() {
|
||||||
|
this(Path.of(System.getProperty("user.home")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// only exists for testing
|
||||||
|
HttpClientBuilder(Path userHome) {
|
||||||
|
this.userHome = userHome;
|
||||||
|
var release = Release.current();
|
||||||
|
userAgent = "Pkl/" + release.version() + " (" + release.os() + "; " + release.flavor() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient.Builder setUserAgent(String userAgent) {
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient.Builder setConnectTimeout(Duration timeout) {
|
||||||
|
this.connectTimeout = timeout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient.Builder setRequestTimeout(Duration timeout) {
|
||||||
|
this.requestTimeout = timeout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient.Builder addCertificates(Path file) {
|
||||||
|
certificateFiles.add(file);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient.Builder addCertificates(URI url) {
|
||||||
|
var scheme = url.getScheme();
|
||||||
|
if (!"jar".equalsIgnoreCase(scheme) && !"file".equalsIgnoreCase(scheme)) {
|
||||||
|
throw new HttpClientInitException(ErrorMessages.create("expectedJarOrFileUrl", url));
|
||||||
|
}
|
||||||
|
certificateUris.add(url);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient.Builder addDefaultCliCertificates() {
|
||||||
|
var directory = userHome.resolve(".pkl").resolve("cacerts");
|
||||||
|
var fileCount = certificateFiles.size();
|
||||||
|
if (Files.isDirectory(directory)) {
|
||||||
|
try (var files = Files.list(directory)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient build() {
|
||||||
|
return doBuild().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient buildLazily() {
|
||||||
|
return new LazyHttpClient(doBuild());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Supplier<HttpClient> doBuild() {
|
||||||
|
// make defensive copies because Supplier may get called after builder was mutated
|
||||||
|
var certificateFiles = List.copyOf(this.certificateFiles);
|
||||||
|
var certificateUris = List.copyOf(this.certificateUris);
|
||||||
|
return () -> {
|
||||||
|
var jdkClient = new JdkHttpClient(certificateFiles, certificateUris, connectTimeout);
|
||||||
|
return new RequestRewritingClient(userAgent, requestTimeout, 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.core.http;
|
||||||
|
|
||||||
|
import org.pkl.core.PklException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an error occurred while initializing an HTTP client. A common example is an error
|
||||||
|
* reading or parsing a certificate.
|
||||||
|
*/
|
||||||
|
public class HttpClientInitException extends PklException {
|
||||||
|
public HttpClientInitException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientInitException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClientInitException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
206
pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java
Normal file
206
pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.core.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.net.ConnectException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.net.http.HttpResponse.BodyHandler;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.CertPathBuilder;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.PKIXBuilderParameters;
|
||||||
|
import java.security.cert.PKIXRevocationChecker;
|
||||||
|
import java.security.cert.TrustAnchor;
|
||||||
|
import java.security.cert.X509CertSelector;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
|
import javax.net.ssl.CertPathTrustManagerParameters;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
import org.pkl.core.util.ErrorMessages;
|
||||||
|
import org.pkl.core.util.Exceptions;
|
||||||
|
|
||||||
|
/** An {@code HttpClient} implementation backed by {@link java.net.http.HttpClient}. */
|
||||||
|
@ThreadSafe
|
||||||
|
final class JdkHttpClient implements HttpClient {
|
||||||
|
// non-private for testing
|
||||||
|
final java.net.http.HttpClient underlying;
|
||||||
|
|
||||||
|
// call java.net.http.HttpClient.close() if available (JDK 21+)
|
||||||
|
private static final MethodHandle closeMethod;
|
||||||
|
|
||||||
|
static {
|
||||||
|
var methodType = MethodType.methodType(void.class, java.net.http.HttpClient.class);
|
||||||
|
MethodHandle result;
|
||||||
|
try {
|
||||||
|
//noinspection JavaLangInvokeHandleSignature
|
||||||
|
result =
|
||||||
|
MethodHandles.publicLookup()
|
||||||
|
.findVirtual(java.net.http.HttpClient.class, "close", methodType);
|
||||||
|
} catch (NoSuchMethodException | IllegalAccessException e) {
|
||||||
|
// use no-op close method
|
||||||
|
result = MethodHandles.empty(methodType);
|
||||||
|
}
|
||||||
|
closeMethod = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
JdkHttpClient(List<Path> certificateFiles, List<URI> certificateUris, Duration connectTimeout) {
|
||||||
|
underlying =
|
||||||
|
java.net.http.HttpClient.newBuilder()
|
||||||
|
.sslContext(createSslContext(certificateFiles, certificateUris))
|
||||||
|
.connectTimeout(connectTimeout)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||||
|
throws IOException {
|
||||||
|
try {
|
||||||
|
return underlying.send(request, responseBodyHandler);
|
||||||
|
} catch (ConnectException e) {
|
||||||
|
// original exception has no message
|
||||||
|
throw new ConnectException(
|
||||||
|
ErrorMessages.create("errorConnectingToHost", request.uri().getHost()));
|
||||||
|
} catch (SSLHandshakeException e) {
|
||||||
|
throw new SSLHandshakeException(
|
||||||
|
ErrorMessages.create(
|
||||||
|
"errorSslHandshake", request.uri().getHost(), Exceptions.getRootReason(e)));
|
||||||
|
} catch (SSLException e) {
|
||||||
|
throw new SSLException(Exceptions.getRootReason(e));
|
||||||
|
} catch (IOException e) {
|
||||||
|
// JDK 11 throws IOException instead of SSLHandshakeException
|
||||||
|
throw new IOException(Exceptions.getRootReason(e));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// next best thing after letting (checked) InterruptedException bubble up
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
closeMethod.invoke(underlying);
|
||||||
|
} catch (RuntimeException | Error e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw new AssertionError(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#security-algorithm-implementation-requirements
|
||||||
|
private static SSLContext createSslContext(
|
||||||
|
List<Path> certificateFiles, List<URI> certificateUris) {
|
||||||
|
try {
|
||||||
|
if (certificateFiles.isEmpty() && certificateUris.isEmpty()) {
|
||||||
|
// fall back to JVM defaults (not Pkl built-in certs)
|
||||||
|
return SSLContext.getDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
var certPathBuilder = CertPathBuilder.getInstance("PKIX");
|
||||||
|
// create a non-legacy revocation checker that is configured via setOptions() instead of
|
||||||
|
// security property "ocsp.enabled"
|
||||||
|
var revocationChecker = (PKIXRevocationChecker) certPathBuilder.getRevocationChecker();
|
||||||
|
revocationChecker.setOptions(Set.of()); // prefer OCSP, fall back to CRLs
|
||||||
|
|
||||||
|
var certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
Set<TrustAnchor> trustAnchors =
|
||||||
|
createTrustAnchors(certFactory, certificateFiles, certificateUris);
|
||||||
|
var pkixParameters = new PKIXBuilderParameters(trustAnchors, new X509CertSelector());
|
||||||
|
// equivalent of "com.sun.net.ssl.checkRevocation=true"
|
||||||
|
pkixParameters.setRevocationEnabled(true);
|
||||||
|
pkixParameters.addCertPathChecker(revocationChecker);
|
||||||
|
|
||||||
|
var trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
|
||||||
|
trustManagerFactory.init(new CertPathTrustManagerParameters(pkixParameters));
|
||||||
|
|
||||||
|
var sslContext = SSLContext.getInstance("TLS");
|
||||||
|
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
|
||||||
|
|
||||||
|
return sslContext;
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new HttpClientInitException(
|
||||||
|
ErrorMessages.create("cannotInitHttpClient", Exceptions.getRootReason(e)), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<TrustAnchor> createTrustAnchors(
|
||||||
|
CertificateFactory factory, List<Path> certificateFiles, List<URI> certificateUris) {
|
||||||
|
var anchors = new HashSet<TrustAnchor>();
|
||||||
|
|
||||||
|
for (var file : certificateFiles) {
|
||||||
|
try (var stream = Files.newInputStream(file)) {
|
||||||
|
collectTrustAnchors(anchors, factory, stream, file);
|
||||||
|
} catch (NoSuchFileException e) {
|
||||||
|
throw new HttpClientInitException(ErrorMessages.create("cannotFindCertFile", file));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new HttpClientInitException(
|
||||||
|
ErrorMessages.create("cannotReadCertFile", Exceptions.getRootReason(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var uri : certificateUris) {
|
||||||
|
try (var stream = uri.toURL().openStream()) {
|
||||||
|
collectTrustAnchors(anchors, factory, stream, uri);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new HttpClientInitException(
|
||||||
|
ErrorMessages.create("cannotReadCertFile", Exceptions.getRootReason(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return anchors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectTrustAnchors(
|
||||||
|
Collection<TrustAnchor> anchors,
|
||||||
|
CertificateFactory factory,
|
||||||
|
InputStream stream,
|
||||||
|
Object source) {
|
||||||
|
Collection<X509Certificate> certificates;
|
||||||
|
try {
|
||||||
|
//noinspection unchecked
|
||||||
|
certificates = (Collection<X509Certificate>) factory.generateCertificates(stream);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new HttpClientInitException(
|
||||||
|
ErrorMessages.create("cannotParseCertFile", source, Exceptions.getRootReason(e)));
|
||||||
|
}
|
||||||
|
if (certificates.isEmpty()) {
|
||||||
|
throw new HttpClientInitException(ErrorMessages.create("emptyCertFile", source));
|
||||||
|
}
|
||||||
|
for (var certificate : certificates) {
|
||||||
|
anchors.add(new TrustAnchor(certificate, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
pkl-core/src/main/java/org/pkl/core/http/LazyHttpClient.java
Normal file
81
pkl-core/src/main/java/org/pkl/core/http/LazyHttpClient.java
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.core.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.net.http.HttpResponse.BodyHandler;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@code HttpClient} decorator that defers creating the underlying HTTP client until the first
|
||||||
|
* send.
|
||||||
|
*/
|
||||||
|
@ThreadSafe
|
||||||
|
class LazyHttpClient implements HttpClient {
|
||||||
|
private final Supplier<HttpClient> supplier;
|
||||||
|
private final Object lock = new Object();
|
||||||
|
|
||||||
|
@GuardedBy("lock")
|
||||||
|
private HttpClient client;
|
||||||
|
|
||||||
|
@GuardedBy("lock")
|
||||||
|
private RuntimeException exception;
|
||||||
|
|
||||||
|
LazyHttpClient(Supplier<HttpClient> supplier) {
|
||||||
|
this.supplier = supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||||
|
throws IOException {
|
||||||
|
return getOrCreateClient().send(request, responseBodyHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
getClient().ifPresent(HttpClient::close);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpClient getOrCreateClient() {
|
||||||
|
synchronized (lock) {
|
||||||
|
// only try to create client once
|
||||||
|
if (exception != null) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client == null) {
|
||||||
|
try {
|
||||||
|
client = supplier.get();
|
||||||
|
} catch (RuntimeException t) {
|
||||||
|
exception = t;
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<HttpClient> getClient() {
|
||||||
|
synchronized (lock) {
|
||||||
|
return Optional.ofNullable(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.core.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.net.http.HttpResponse.BodyHandler;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@code HttpClient} decorator that
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>overrides the {@code User-Agent} header of {@code HttpRequest}s
|
||||||
|
* <li>sets a request timeout if none is present
|
||||||
|
* <li>ensures that {@link #close()} is idempotent.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Both {@code User-Agent} header and default request timeout are configurable through {@link
|
||||||
|
* HttpClient.Builder}.
|
||||||
|
*/
|
||||||
|
@ThreadSafe
|
||||||
|
final class RequestRewritingClient implements HttpClient {
|
||||||
|
// non-private for testing
|
||||||
|
final String userAgent;
|
||||||
|
final Duration requestTimeout;
|
||||||
|
final HttpClient delegate;
|
||||||
|
|
||||||
|
private final AtomicBoolean closed = new AtomicBoolean();
|
||||||
|
|
||||||
|
RequestRewritingClient(String userAgent, Duration requestTimeout, HttpClient delegate) {
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
this.requestTimeout = requestTimeout;
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||||
|
throws IOException {
|
||||||
|
checkNotClosed(request);
|
||||||
|
return delegate.send(rewriteRequest(request), responseBodyHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (!closed.getAndSet(true)) delegate.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on JDK 17's implementation of HttpRequest.newBuilder(HttpRequest, filter).
|
||||||
|
private HttpRequest rewriteRequest(HttpRequest original) {
|
||||||
|
HttpRequest.Builder builder = HttpRequest.newBuilder();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.uri(original.uri())
|
||||||
|
.expectContinue(original.expectContinue())
|
||||||
|
.timeout(original.timeout().orElse(requestTimeout))
|
||||||
|
.version(original.version().orElse(java.net.http.HttpClient.Version.HTTP_2));
|
||||||
|
|
||||||
|
original
|
||||||
|
.headers()
|
||||||
|
.map()
|
||||||
|
.forEach((name, values) -> values.forEach(value -> builder.header(name, value)));
|
||||||
|
builder.setHeader("User-Agent", userAgent);
|
||||||
|
|
||||||
|
var method = original.method();
|
||||||
|
original
|
||||||
|
.bodyPublisher()
|
||||||
|
.ifPresentOrElse(
|
||||||
|
publisher -> builder.method(method, publisher),
|
||||||
|
() -> {
|
||||||
|
switch (method) {
|
||||||
|
case "GET":
|
||||||
|
builder.GET();
|
||||||
|
break;
|
||||||
|
case "DELETE":
|
||||||
|
builder.DELETE();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder.method(method, HttpRequest.BodyPublishers.noBody());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkNotClosed(HttpRequest request) {
|
||||||
|
if (closed.get()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Cannot send request " + request + " because this client has already been closed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse.BodyHandlers;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -34,6 +36,7 @@ import org.pkl.core.packages.PackageLoadError;
|
|||||||
import org.pkl.core.packages.PackageResolver;
|
import org.pkl.core.packages.PackageResolver;
|
||||||
import org.pkl.core.runtime.VmContext;
|
import org.pkl.core.runtime.VmContext;
|
||||||
import org.pkl.core.util.ErrorMessages;
|
import org.pkl.core.util.ErrorMessages;
|
||||||
|
import org.pkl.core.util.HttpUtils;
|
||||||
import org.pkl.core.util.IoUtils;
|
import org.pkl.core.util.IoUtils;
|
||||||
import org.pkl.core.util.Nullable;
|
import org.pkl.core.util.Nullable;
|
||||||
import org.pkl.core.util.Pair;
|
import org.pkl.core.util.Pair;
|
||||||
@@ -481,6 +484,19 @@ public final class ModuleKeys {
|
|||||||
throws IOException, SecurityManagerException {
|
throws IOException, SecurityManagerException {
|
||||||
securityManager.checkResolveModule(uri);
|
securityManager.checkResolveModule(uri);
|
||||||
|
|
||||||
|
if (HttpUtils.isHttpUrl(uri)) {
|
||||||
|
var httpClient = VmContext.get(null).getHttpClient();
|
||||||
|
var request = HttpRequest.newBuilder(uri).build();
|
||||||
|
var response = httpClient.send(request, BodyHandlers.ofInputStream());
|
||||||
|
try (var body = response.body()) {
|
||||||
|
HttpUtils.checkHasStatusCode200(response);
|
||||||
|
securityManager.checkResolveModule(response.uri());
|
||||||
|
String text = IoUtils.readString(body);
|
||||||
|
// intentionally use uri instead of response.uri()
|
||||||
|
return ResolvedModuleKeys.virtual(this, uri, text, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var url = IoUtils.toUrl(uri);
|
var url = IoUtils.toUrl(uri);
|
||||||
var conn = url.openConnection();
|
var conn = url.openConnection();
|
||||||
conn.connect();
|
conn.connect();
|
||||||
@@ -494,6 +510,7 @@ public final class ModuleKeys {
|
|||||||
}
|
}
|
||||||
securityManager.checkResolveModule(redirected);
|
securityManager.checkResolveModule(redirected);
|
||||||
var text = IoUtils.readString(stream);
|
var text = IoUtils.readString(stream);
|
||||||
|
// intentionally use uri instead of redirected
|
||||||
return ResolvedModuleKeys.virtual(this, uri, text, true);
|
return ResolvedModuleKeys.virtual(this, uri, text, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ package org.pkl.core.module;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse.BodyHandlers;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import org.pkl.core.runtime.VmContext;
|
||||||
|
import org.pkl.core.util.HttpUtils;
|
||||||
import org.pkl.core.util.IoUtils;
|
import org.pkl.core.util.IoUtils;
|
||||||
|
|
||||||
/** Utilities for obtaining and using resolved module keys. */
|
/** Utilities for obtaining and using resolved module keys. */
|
||||||
@@ -102,6 +106,14 @@ public final class ResolvedModuleKeys {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String loadSource() throws IOException {
|
public String loadSource() throws IOException {
|
||||||
|
if (HttpUtils.isHttpUrl(url)) {
|
||||||
|
var httpClient = VmContext.get(null).getHttpClient();
|
||||||
|
var request = HttpRequest.newBuilder(uri).build();
|
||||||
|
var response = httpClient.send(request, BodyHandlers.ofString());
|
||||||
|
HttpUtils.checkHasStatusCode200(response);
|
||||||
|
return response.body();
|
||||||
|
}
|
||||||
|
|
||||||
return IoUtils.readString(url);
|
return IoUtils.readString(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import java.util.List;
|
|||||||
import javax.naming.OperationNotSupportedException;
|
import javax.naming.OperationNotSupportedException;
|
||||||
import org.pkl.core.SecurityManager;
|
import org.pkl.core.SecurityManager;
|
||||||
import org.pkl.core.SecurityManagerException;
|
import org.pkl.core.SecurityManagerException;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.PathElement;
|
import org.pkl.core.module.PathElement;
|
||||||
import org.pkl.core.packages.PackageResolvers.DiskCachedPackageResolver;
|
import org.pkl.core.packages.PackageResolvers.DiskCachedPackageResolver;
|
||||||
import org.pkl.core.packages.PackageResolvers.InMemoryPackageResolver;
|
import org.pkl.core.packages.PackageResolvers.InMemoryPackageResolver;
|
||||||
@@ -30,10 +31,11 @@ import org.pkl.core.util.Pair;
|
|||||||
|
|
||||||
public interface PackageResolver extends Closeable {
|
public interface PackageResolver extends Closeable {
|
||||||
|
|
||||||
static PackageResolver getInstance(SecurityManager securityManager, @Nullable Path cachedDir) {
|
static PackageResolver getInstance(
|
||||||
|
SecurityManager securityManager, HttpClient httpClient, @Nullable Path cachedDir) {
|
||||||
return cachedDir == null
|
return cachedDir == null
|
||||||
? new InMemoryPackageResolver(securityManager)
|
? new InMemoryPackageResolver(securityManager, httpClient)
|
||||||
: new DiskCachedPackageResolver(securityManager, cachedDir);
|
: new DiskCachedPackageResolver(securityManager, httpClient, cachedDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
DependencyMetadata getDependencyMetadata(PackageUri uri, @Nullable Checksums checksums)
|
DependencyMetadata getDependencyMetadata(PackageUri uri, @Nullable Checksums checksums)
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.net.http.HttpResponse.BodyHandlers;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
@@ -41,11 +44,10 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
import javax.annotation.concurrent.GuardedBy;
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
import org.graalvm.collections.EconomicMap;
|
import org.graalvm.collections.EconomicMap;
|
||||||
import org.pkl.core.Release;
|
|
||||||
import org.pkl.core.SecurityManager;
|
import org.pkl.core.SecurityManager;
|
||||||
import org.pkl.core.SecurityManagerException;
|
import org.pkl.core.SecurityManagerException;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.FileResolver;
|
import org.pkl.core.module.FileResolver;
|
||||||
import org.pkl.core.module.PathElement;
|
import org.pkl.core.module.PathElement;
|
||||||
import org.pkl.core.module.PathElement.TreePathElement;
|
import org.pkl.core.module.PathElement.TreePathElement;
|
||||||
@@ -53,6 +55,7 @@ import org.pkl.core.runtime.FileSystemManager;
|
|||||||
import org.pkl.core.runtime.VmExceptionBuilder;
|
import org.pkl.core.runtime.VmExceptionBuilder;
|
||||||
import org.pkl.core.util.ByteArrayUtils;
|
import org.pkl.core.util.ByteArrayUtils;
|
||||||
import org.pkl.core.util.EconomicMaps;
|
import org.pkl.core.util.EconomicMaps;
|
||||||
|
import org.pkl.core.util.HttpUtils;
|
||||||
import org.pkl.core.util.IoUtils;
|
import org.pkl.core.util.IoUtils;
|
||||||
import org.pkl.core.util.Nullable;
|
import org.pkl.core.util.Nullable;
|
||||||
import org.pkl.core.util.Pair;
|
import org.pkl.core.util.Pair;
|
||||||
@@ -60,24 +63,20 @@ import org.pkl.core.util.json.Json.JsonParseException;
|
|||||||
|
|
||||||
class PackageResolvers {
|
class PackageResolvers {
|
||||||
abstract static class AbstractPackageResolver implements PackageResolver {
|
abstract static class AbstractPackageResolver implements PackageResolver {
|
||||||
private static final String USER_AGENT;
|
|
||||||
|
|
||||||
static {
|
|
||||||
var release = Release.current();
|
|
||||||
USER_AGENT = "Pkl/" + release.version() + " (" + release.os() + "; " + release.flavor() + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GuardedBy("lock")
|
@GuardedBy("lock")
|
||||||
private final EconomicMap<PackageUri, DependencyMetadata> cachedDependencyMetadata;
|
private final EconomicMap<PackageUri, DependencyMetadata> cachedDependencyMetadata;
|
||||||
|
|
||||||
private final SecurityManager securityManager;
|
private final SecurityManager securityManager;
|
||||||
|
|
||||||
|
protected final HttpClient httpClient;
|
||||||
|
|
||||||
private final AtomicBoolean isClosed = new AtomicBoolean();
|
private final AtomicBoolean isClosed = new AtomicBoolean();
|
||||||
|
|
||||||
protected final Object lock = new Object();
|
protected final Object lock = new Object();
|
||||||
|
|
||||||
protected AbstractPackageResolver(SecurityManager securityManager) {
|
protected AbstractPackageResolver(SecurityManager securityManager, HttpClient httpClient) {
|
||||||
this.securityManager = securityManager;
|
this.securityManager = securityManager;
|
||||||
|
this.httpClient = httpClient;
|
||||||
cachedDependencyMetadata = EconomicMaps.create();
|
cachedDependencyMetadata = EconomicMaps.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,21 +188,26 @@ class PackageResolvers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InputStream openExternalUri(URI uri) throws SecurityManagerException, IOException {
|
protected InputStream openExternalUri(URI uri) throws SecurityManagerException {
|
||||||
|
if (!HttpUtils.isHttpUrl(uri)) {
|
||||||
|
throw new IllegalArgumentException("Expected HTTP(S) URL, but got: " + uri);
|
||||||
|
}
|
||||||
|
|
||||||
// treat package assets as resources instead of modules
|
// treat package assets as resources instead of modules
|
||||||
securityManager.checkReadResource(uri);
|
securityManager.checkReadResource(uri);
|
||||||
var connection = (HttpsURLConnection) uri.toURL().openConnection();
|
var request = HttpRequest.newBuilder(uri).build();
|
||||||
connection.setRequestProperty("User-Agent", USER_AGENT);
|
HttpResponse<InputStream> response;
|
||||||
int responseCode;
|
|
||||||
try {
|
try {
|
||||||
responseCode = connection.getResponseCode();
|
response = httpClient.send(request, BodyHandlers.ofInputStream());
|
||||||
if (responseCode != 200) {
|
|
||||||
throw new PackageLoadError("badHttpStatusCode", responseCode, uri);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new PackageLoadError(e, "ioErrorMakingHttpGet", uri, e.getMessage());
|
throw new PackageLoadError(e, "ioErrorMakingHttpGet", uri, e.getMessage());
|
||||||
}
|
}
|
||||||
return connection.getInputStream();
|
try {
|
||||||
|
HttpUtils.checkHasStatusCode200(response);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PackageLoadError("badHttpStatusCode", response.statusCode(), response.uri());
|
||||||
|
}
|
||||||
|
return response.body();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected IOException fileIsADirectory() {
|
protected IOException fileIsADirectory() {
|
||||||
@@ -251,8 +255,8 @@ class PackageResolvers {
|
|||||||
private final EconomicMap<PackageUri, TreePathElement> cachedTreePathElementRoots =
|
private final EconomicMap<PackageUri, TreePathElement> cachedTreePathElementRoots =
|
||||||
EconomicMaps.create();
|
EconomicMaps.create();
|
||||||
|
|
||||||
InMemoryPackageResolver(SecurityManager securityManager) {
|
InMemoryPackageResolver(SecurityManager securityManager, HttpClient httpClient) {
|
||||||
super(securityManager);
|
super(securityManager, httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getPackageBytes(PackageUri packageUri, DependencyMetadata metadata)
|
private byte[] getPackageBytes(PackageUri packageUri, DependencyMetadata metadata)
|
||||||
@@ -419,8 +423,9 @@ class PackageResolvers {
|
|||||||
PosixFilePermission.GROUP_READ,
|
PosixFilePermission.GROUP_READ,
|
||||||
PosixFilePermission.OTHERS_READ);
|
PosixFilePermission.OTHERS_READ);
|
||||||
|
|
||||||
public DiskCachedPackageResolver(SecurityManager securityManager, Path cacheDir) {
|
public DiskCachedPackageResolver(
|
||||||
super(securityManager);
|
SecurityManager securityManager, HttpClient httpClient, Path cacheDir) {
|
||||||
|
super(securityManager, httpClient);
|
||||||
this.cacheDir = cacheDir;
|
this.cacheDir = cacheDir;
|
||||||
this.tmpDir = cacheDir.resolve("tmp");
|
this.tmpDir = cacheDir.resolve("tmp");
|
||||||
}
|
}
|
||||||
@@ -463,8 +468,8 @@ class PackageResolvers {
|
|||||||
if (checksums != null) {
|
if (checksums != null) {
|
||||||
inputStream = newDigestInputStream(inputStream);
|
inputStream = newDigestInputStream(inputStream);
|
||||||
}
|
}
|
||||||
Files.createDirectories(path.getParent());
|
|
||||||
try (var in = inputStream) {
|
try (var in = inputStream) {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
Files.copy(in, path);
|
Files.copy(in, path);
|
||||||
if (checksums != null) {
|
if (checksums != null) {
|
||||||
var digestInputStream = (DigestInputStream) inputStream;
|
var digestInputStream = (DigestInputStream) inputStream;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import org.pkl.core.SecurityManager;
|
|||||||
import org.pkl.core.SecurityManagerException;
|
import org.pkl.core.SecurityManagerException;
|
||||||
import org.pkl.core.StackFrameTransformer;
|
import org.pkl.core.StackFrameTransformer;
|
||||||
import org.pkl.core.ast.builder.ImportsAndReadsParser;
|
import org.pkl.core.ast.builder.ImportsAndReadsParser;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.ModuleKeys;
|
import org.pkl.core.module.ModuleKeys;
|
||||||
import org.pkl.core.module.ProjectDependenciesManager;
|
import org.pkl.core.module.ProjectDependenciesManager;
|
||||||
import org.pkl.core.module.ResolvedModuleKeys;
|
import org.pkl.core.module.ResolvedModuleKeys;
|
||||||
@@ -105,6 +106,7 @@ public class ProjectPackager {
|
|||||||
String outputPathPattern,
|
String outputPathPattern,
|
||||||
StackFrameTransformer stackFrameTransformer,
|
StackFrameTransformer stackFrameTransformer,
|
||||||
SecurityManager securityManager,
|
SecurityManager securityManager,
|
||||||
|
HttpClient httpClient,
|
||||||
boolean skipPublishCheck,
|
boolean skipPublishCheck,
|
||||||
Writer outputWriter) {
|
Writer outputWriter) {
|
||||||
this.projects = projects;
|
this.projects = projects;
|
||||||
@@ -112,7 +114,7 @@ public class ProjectPackager {
|
|||||||
this.outputPathPattern = outputPathPattern;
|
this.outputPathPattern = outputPathPattern;
|
||||||
this.stackFrameTransformer = stackFrameTransformer;
|
this.stackFrameTransformer = stackFrameTransformer;
|
||||||
// intentionally use InMemoryPackageResolver
|
// intentionally use InMemoryPackageResolver
|
||||||
this.packageResolver = PackageResolver.getInstance(securityManager, null);
|
this.packageResolver = PackageResolver.getInstance(securityManager, httpClient, null);
|
||||||
this.skipPublishCheck = skipPublishCheck;
|
this.skipPublishCheck = skipPublishCheck;
|
||||||
this.outputWriter = outputWriter;
|
this.outputWriter = outputWriter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.pkl.core.ast.builder.AstBuilder;
|
|||||||
import org.pkl.core.ast.member.*;
|
import org.pkl.core.ast.member.*;
|
||||||
import org.pkl.core.ast.repl.ResolveClassMemberNode;
|
import org.pkl.core.ast.repl.ResolveClassMemberNode;
|
||||||
import org.pkl.core.ast.type.TypeNode;
|
import org.pkl.core.ast.type.TypeNode;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.*;
|
import org.pkl.core.module.*;
|
||||||
import org.pkl.core.packages.PackageResolver;
|
import org.pkl.core.packages.PackageResolver;
|
||||||
import org.pkl.core.parser.LexParseException;
|
import org.pkl.core.parser.LexParseException;
|
||||||
@@ -67,6 +68,7 @@ public class ReplServer implements AutoCloseable {
|
|||||||
|
|
||||||
public ReplServer(
|
public ReplServer(
|
||||||
SecurityManager securityManager,
|
SecurityManager securityManager,
|
||||||
|
HttpClient httpClient,
|
||||||
Logger logger,
|
Logger logger,
|
||||||
Collection<ModuleKeyFactory> moduleKeyFactories,
|
Collection<ModuleKeyFactory> moduleKeyFactories,
|
||||||
Collection<ResourceReader> resourceReaders,
|
Collection<ResourceReader> resourceReaders,
|
||||||
@@ -85,7 +87,7 @@ public class ReplServer implements AutoCloseable {
|
|||||||
replState = new ReplState(createEmptyReplModule(BaseModule.getModuleClass().getPrototype()));
|
replState = new ReplState(createEmptyReplModule(BaseModule.getModuleClass().getPrototype()));
|
||||||
|
|
||||||
var languageRef = new MutableReference<VmLanguage>(null);
|
var languageRef = new MutableReference<VmLanguage>(null);
|
||||||
packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir);
|
packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir);
|
||||||
projectDependenciesManager =
|
projectDependenciesManager =
|
||||||
projectDependencies == null ? null : new ProjectDependenciesManager(projectDependencies);
|
projectDependencies == null ? null : new ProjectDependenciesManager(projectDependencies);
|
||||||
polyglotContext =
|
polyglotContext =
|
||||||
@@ -97,6 +99,7 @@ public class ReplServer implements AutoCloseable {
|
|||||||
new VmContext.Holder(
|
new VmContext.Holder(
|
||||||
frameTransformer,
|
frameTransformer,
|
||||||
securityManager,
|
securityManager,
|
||||||
|
httpClient,
|
||||||
moduleResolver,
|
moduleResolver,
|
||||||
new ResourceManager(securityManager, resourceReaders),
|
new ResourceManager(securityManager, resourceReaders),
|
||||||
logger,
|
logger,
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse.BodyHandlers;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -38,6 +40,7 @@ import org.pkl.core.packages.PackageAssetUri;
|
|||||||
import org.pkl.core.packages.PackageResolver;
|
import org.pkl.core.packages.PackageResolver;
|
||||||
import org.pkl.core.runtime.VmContext;
|
import org.pkl.core.runtime.VmContext;
|
||||||
import org.pkl.core.util.ErrorMessages;
|
import org.pkl.core.util.ErrorMessages;
|
||||||
|
import org.pkl.core.util.HttpUtils;
|
||||||
import org.pkl.core.util.IoUtils;
|
import org.pkl.core.util.IoUtils;
|
||||||
import org.pkl.core.util.Nullable;
|
import org.pkl.core.util.Nullable;
|
||||||
|
|
||||||
@@ -291,6 +294,15 @@ public final class ResourceReaders {
|
|||||||
private abstract static class UrlResource implements ResourceReader {
|
private abstract static class UrlResource implements ResourceReader {
|
||||||
@Override
|
@Override
|
||||||
public Optional<Object> read(URI uri) throws IOException {
|
public Optional<Object> read(URI uri) throws IOException {
|
||||||
|
if (HttpUtils.isHttpUrl(uri)) {
|
||||||
|
var httpClient = VmContext.get(null).getHttpClient();
|
||||||
|
var request = HttpRequest.newBuilder(uri).build();
|
||||||
|
var response = httpClient.send(request, BodyHandlers.ofByteArray());
|
||||||
|
if (response.statusCode() == 404) return Optional.empty();
|
||||||
|
HttpUtils.checkHasStatusCode200(response);
|
||||||
|
return Optional.of(new Resource(uri, response.body()));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var url = IoUtils.toUrl(uri);
|
var url = IoUtils.toUrl(uri);
|
||||||
var content = IoUtils.readBytes(url);
|
var content = IoUtils.readBytes(url);
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.pkl.core.runtime;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
|
||||||
|
|
||||||
public class CertificateUtils {
|
|
||||||
public static void setupAllX509CertificatesGlobally(List<Object> certs) {
|
|
||||||
try {
|
|
||||||
var certificates = new ArrayList<X509Certificate>(certs.size());
|
|
||||||
for (var cert : certs) {
|
|
||||||
try (var input = toInputStream(cert)) {
|
|
||||||
certificates.addAll(generateCertificates(input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setupX509CertificatesGlobally(certificates);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InputStream toInputStream(Object cert) throws IOException {
|
|
||||||
if (cert instanceof Path) {
|
|
||||||
var pathCert = (Path) cert;
|
|
||||||
return Files.newInputStream(pathCert);
|
|
||||||
}
|
|
||||||
if (cert instanceof InputStream) {
|
|
||||||
return (InputStream) cert;
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Unknown class for certificate: "
|
|
||||||
+ cert.getClass()
|
|
||||||
+ ". Valid types: java.nio.Path, java.io.InputStream");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Collection<X509Certificate> generateCertificates(InputStream inputStream)
|
|
||||||
throws CertificateException {
|
|
||||||
//noinspection unchecked
|
|
||||||
return (Collection<X509Certificate>)
|
|
||||||
CertificateFactory.getInstance("X.509").generateCertificates(inputStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setupX509CertificatesGlobally(Collection<X509Certificate> certs)
|
|
||||||
throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException,
|
|
||||||
KeyManagementException {
|
|
||||||
System.setProperty("com.sun.net.ssl.checkRevocation", "true");
|
|
||||||
Security.setProperty("ocsp.enable", "true");
|
|
||||||
var keystore = KeyStore.getInstance(KeyStore.getDefaultType());
|
|
||||||
keystore.load(null);
|
|
||||||
|
|
||||||
var count = 1;
|
|
||||||
for (var cert : certs) {
|
|
||||||
keystore.setCertificateEntry("Certificate" + count++, cert);
|
|
||||||
}
|
|
||||||
var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
|
||||||
tmf.init(keystore);
|
|
||||||
|
|
||||||
var sc = SSLContext.getInstance("SSL");
|
|
||||||
sc.init(null, tmf.getTrustManagers(), new SecureRandom());
|
|
||||||
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.pkl.core.SecurityManager;
|
import org.pkl.core.SecurityManager;
|
||||||
import org.pkl.core.SecurityManagerException;
|
import org.pkl.core.SecurityManagerException;
|
||||||
|
import org.pkl.core.http.HttpClientInitException;
|
||||||
import org.pkl.core.module.ModuleKey;
|
import org.pkl.core.module.ModuleKey;
|
||||||
import org.pkl.core.packages.PackageLoadError;
|
import org.pkl.core.packages.PackageLoadError;
|
||||||
import org.pkl.core.resource.Resource;
|
import org.pkl.core.resource.Resource;
|
||||||
@@ -105,7 +106,7 @@ public final class ResourceManager {
|
|||||||
.withHint(e.getMessage())
|
.withHint(e.getMessage())
|
||||||
.withLocation(readNode)
|
.withLocation(readNode)
|
||||||
.build();
|
.build();
|
||||||
} catch (SecurityManagerException e) {
|
} catch (SecurityManagerException | HttpClientInitException e) {
|
||||||
throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build();
|
throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new VmExceptionBuilder()
|
throw new VmExceptionBuilder()
|
||||||
@@ -151,7 +152,7 @@ public final class ResourceManager {
|
|||||||
.withHint(e.getReason())
|
.withHint(e.getReason())
|
||||||
.withLocation(readNode)
|
.withLocation(readNode)
|
||||||
.build();
|
.build();
|
||||||
} catch (SecurityManagerException | PackageLoadError e) {
|
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
|
||||||
throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build();
|
throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build();
|
||||||
}
|
}
|
||||||
if (resource.isEmpty()) return resource;
|
if (resource.isEmpty()) return resource;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import java.util.Map;
|
|||||||
import org.pkl.core.Loggers;
|
import org.pkl.core.Loggers;
|
||||||
import org.pkl.core.SecurityManagers;
|
import org.pkl.core.SecurityManagers;
|
||||||
import org.pkl.core.StackFrameTransformers;
|
import org.pkl.core.StackFrameTransformers;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.ModuleKeyFactories;
|
import org.pkl.core.module.ModuleKeyFactories;
|
||||||
import org.pkl.core.module.ModuleKeys;
|
import org.pkl.core.module.ModuleKeys;
|
||||||
import org.pkl.core.module.ResolvedModuleKey;
|
import org.pkl.core.module.ResolvedModuleKey;
|
||||||
@@ -39,6 +40,7 @@ public abstract class StdLibModule {
|
|||||||
new VmContext.Holder(
|
new VmContext.Holder(
|
||||||
StackFrameTransformers.defaultTransformer,
|
StackFrameTransformers.defaultTransformer,
|
||||||
SecurityManagers.defaultManager,
|
SecurityManagers.defaultManager,
|
||||||
|
HttpClient.dummyClient(),
|
||||||
new ModuleResolver(List.of(ModuleKeyFactories.standardLibrary)),
|
new ModuleResolver(List.of(ModuleKeyFactories.standardLibrary)),
|
||||||
new ResourceManager(SecurityManagers.defaultManager, List.of()),
|
new ResourceManager(SecurityManagers.defaultManager, List.of()),
|
||||||
Loggers.noop(),
|
Loggers.noop(),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.util.Map;
|
|||||||
import org.pkl.core.Logger;
|
import org.pkl.core.Logger;
|
||||||
import org.pkl.core.SecurityManager;
|
import org.pkl.core.SecurityManager;
|
||||||
import org.pkl.core.StackFrameTransformer;
|
import org.pkl.core.StackFrameTransformer;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.ProjectDependenciesManager;
|
import org.pkl.core.module.ProjectDependenciesManager;
|
||||||
import org.pkl.core.packages.PackageResolver;
|
import org.pkl.core.packages.PackageResolver;
|
||||||
import org.pkl.core.util.LateInit;
|
import org.pkl.core.util.LateInit;
|
||||||
@@ -39,6 +40,7 @@ public final class VmContext {
|
|||||||
|
|
||||||
private final StackFrameTransformer frameTransformer;
|
private final StackFrameTransformer frameTransformer;
|
||||||
private final SecurityManager securityManager;
|
private final SecurityManager securityManager;
|
||||||
|
private final HttpClient httpClient;
|
||||||
private final ModuleResolver moduleResolver;
|
private final ModuleResolver moduleResolver;
|
||||||
private final ResourceManager resourceManager;
|
private final ResourceManager resourceManager;
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
@@ -52,6 +54,7 @@ public final class VmContext {
|
|||||||
public Holder(
|
public Holder(
|
||||||
StackFrameTransformer frameTransformer,
|
StackFrameTransformer frameTransformer,
|
||||||
SecurityManager securityManager,
|
SecurityManager securityManager,
|
||||||
|
HttpClient httpClient,
|
||||||
ModuleResolver moduleResolver,
|
ModuleResolver moduleResolver,
|
||||||
ResourceManager resourceManager,
|
ResourceManager resourceManager,
|
||||||
Logger logger,
|
Logger logger,
|
||||||
@@ -64,6 +67,7 @@ public final class VmContext {
|
|||||||
|
|
||||||
this.frameTransformer = frameTransformer;
|
this.frameTransformer = frameTransformer;
|
||||||
this.securityManager = securityManager;
|
this.securityManager = securityManager;
|
||||||
|
this.httpClient = httpClient;
|
||||||
this.moduleResolver = moduleResolver;
|
this.moduleResolver = moduleResolver;
|
||||||
this.resourceManager = resourceManager;
|
this.resourceManager = resourceManager;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
@@ -108,6 +112,10 @@ public final class VmContext {
|
|||||||
return holder.securityManager;
|
return holder.securityManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HttpClient getHttpClient() {
|
||||||
|
return holder.httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
public ModuleResolver getModuleResolver() {
|
public ModuleResolver getModuleResolver() {
|
||||||
return holder.moduleResolver;
|
return holder.moduleResolver;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,19 @@ 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.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.pkl.core.*;
|
import org.pkl.core.*;
|
||||||
|
import org.pkl.core.http.HttpClient;
|
||||||
import org.pkl.core.module.ModuleKeyFactories;
|
import org.pkl.core.module.ModuleKeyFactories;
|
||||||
import org.pkl.core.module.ModulePathResolver;
|
import org.pkl.core.module.ModulePathResolver;
|
||||||
import org.pkl.core.project.Project;
|
import org.pkl.core.project.Project;
|
||||||
@@ -28,10 +37,30 @@ import org.pkl.core.resource.ResourceReaders;
|
|||||||
import org.pkl.executor.spi.v1.ExecutorSpi;
|
import org.pkl.executor.spi.v1.ExecutorSpi;
|
||||||
import org.pkl.executor.spi.v1.ExecutorSpiException;
|
import org.pkl.executor.spi.v1.ExecutorSpiException;
|
||||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
|
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
|
||||||
|
import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
|
||||||
|
|
||||||
public class ExecutorSpiImpl implements ExecutorSpi {
|
public class ExecutorSpiImpl implements ExecutorSpi {
|
||||||
|
private static final int MAX_HTTP_CLIENTS = 3;
|
||||||
|
|
||||||
|
// Don't create a new HTTP client for every executor request.
|
||||||
|
// Instead, keep a cache of up to MAX_HTTP_CLIENTS clients.
|
||||||
|
// A cache size of 1 should be common.
|
||||||
|
private final Map<HttpClientKey, HttpClient> httpClients;
|
||||||
|
|
||||||
private final String pklVersion = Release.current().version().toString();
|
private final String pklVersion = Release.current().version().toString();
|
||||||
|
|
||||||
|
public ExecutorSpiImpl() {
|
||||||
|
// only LRU cache available in JDK
|
||||||
|
var map =
|
||||||
|
new LinkedHashMap<HttpClientKey, HttpClient>(8, 0.75f, true) {
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Entry<HttpClientKey, HttpClient> eldest) {
|
||||||
|
return size() > MAX_HTTP_CLIENTS;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
httpClients = Collections.synchronizedMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getPklVersion() {
|
public String getPklVersion() {
|
||||||
return pklVersion;
|
return pklVersion;
|
||||||
@@ -65,6 +94,7 @@ public class ExecutorSpiImpl implements ExecutorSpi {
|
|||||||
EvaluatorBuilder.unconfigured()
|
EvaluatorBuilder.unconfigured()
|
||||||
.setStackFrameTransformer(transformer)
|
.setStackFrameTransformer(transformer)
|
||||||
.setSecurityManager(securityManager)
|
.setSecurityManager(securityManager)
|
||||||
|
.setHttpClient(getOrCreateHttpClient(options))
|
||||||
.addResourceReader(ResourceReaders.environmentVariable())
|
.addResourceReader(ResourceReaders.environmentVariable())
|
||||||
.addResourceReader(ResourceReaders.externalProperty())
|
.addResourceReader(ResourceReaders.externalProperty())
|
||||||
.addResourceReader(ResourceReaders.modulePath(resolver))
|
.addResourceReader(ResourceReaders.modulePath(resolver))
|
||||||
@@ -98,4 +128,61 @@ public class ExecutorSpiImpl implements ExecutorSpi {
|
|||||||
ModuleKeyFactories.closeQuietly(builder.getModuleKeyFactories());
|
ModuleKeyFactories.closeQuietly(builder.getModuleKeyFactories());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HttpClient getOrCreateHttpClient(ExecutorSpiOptions options) {
|
||||||
|
List<Path> certificateFiles;
|
||||||
|
List<URI> certificateUris;
|
||||||
|
if (options instanceof ExecutorSpiOptions2) {
|
||||||
|
var options2 = (ExecutorSpiOptions2) options;
|
||||||
|
certificateFiles = options2.getCertificateFiles();
|
||||||
|
certificateUris = options2.getCertificateUris();
|
||||||
|
} else {
|
||||||
|
certificateFiles = List.of();
|
||||||
|
certificateUris = List.of();
|
||||||
|
}
|
||||||
|
var clientKey = new HttpClientKey(certificateFiles, certificateUris);
|
||||||
|
return httpClients.computeIfAbsent(
|
||||||
|
clientKey,
|
||||||
|
(key) -> {
|
||||||
|
var builder = HttpClient.builder();
|
||||||
|
for (var file : key.certificateFiles) {
|
||||||
|
builder.addCertificates(file);
|
||||||
|
}
|
||||||
|
for (var uri : key.certificateUris) {
|
||||||
|
builder.addCertificates(uri);
|
||||||
|
}
|
||||||
|
// If the above didn't add any certificates,
|
||||||
|
// builder will use the JVM's default SSL context.
|
||||||
|
return builder.buildLazily();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class HttpClientKey {
|
||||||
|
final Set<Path> certificateFiles;
|
||||||
|
final Set<URI> certificateUris;
|
||||||
|
|
||||||
|
HttpClientKey(List<Path> certificateFiles, List<URI> certificateUris) {
|
||||||
|
// also serve as defensive copies
|
||||||
|
this.certificateFiles = Set.copyOf(certificateFiles);
|
||||||
|
this.certificateUris = Set.copyOf(certificateUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HttpClientKey that = (HttpClientKey) obj;
|
||||||
|
return certificateFiles.equals(that.certificateFiles)
|
||||||
|
&& certificateUris.equals(that.certificateUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(certificateFiles, certificateUris);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
pkl-core/src/main/java/org/pkl/core/util/Exceptions.java
Normal file
36
pkl-core/src/main/java/org/pkl/core/util/Exceptions.java
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.core.util;
|
||||||
|
|
||||||
|
public final class Exceptions {
|
||||||
|
private Exceptions() {}
|
||||||
|
|
||||||
|
public static Throwable getRootCause(Throwable t) {
|
||||||
|
var result = t;
|
||||||
|
var cause = result.getCause();
|
||||||
|
while (cause != null) {
|
||||||
|
result = cause;
|
||||||
|
cause = cause.getCause();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getRootReason(Throwable t) {
|
||||||
|
var reason = getRootCause(t).getMessage();
|
||||||
|
if (reason == null || reason.isEmpty()) reason = "(unknown reason)";
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
pkl-core/src/main/java/org/pkl/core/util/HttpUtils.java
Normal file
50
pkl-core/src/main/java/org/pkl/core/util/HttpUtils.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.core.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
|
||||||
|
public final class HttpUtils {
|
||||||
|
private HttpUtils() {}
|
||||||
|
|
||||||
|
public static boolean isHttpUrl(URL url) {
|
||||||
|
var protocol = url.getProtocol();
|
||||||
|
return "https".equalsIgnoreCase(protocol) || "http".equalsIgnoreCase(protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isHttpUrl(URI uri) {
|
||||||
|
var scheme = uri.getScheme();
|
||||||
|
return "https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void checkHasStatusCode200(HttpResponse<?> response) throws IOException {
|
||||||
|
if (response.statusCode() == 200) return;
|
||||||
|
|
||||||
|
var body = response.body();
|
||||||
|
if (body instanceof AutoCloseable) {
|
||||||
|
try {
|
||||||
|
((AutoCloseable) body).close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException(
|
||||||
|
ErrorMessages.create("badHttpStatusCode", response.statusCode(), response.uri()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,9 @@ public final class IoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String readString(URL url) throws IOException {
|
public static String readString(URL url) throws IOException {
|
||||||
|
if (HttpUtils.isHttpUrl(url)) {
|
||||||
|
throw new IllegalArgumentException("Should use HTTP client to GET " + url);
|
||||||
|
}
|
||||||
try (var stream = url.openStream()) {
|
try (var stream = url.openStream()) {
|
||||||
return readString(stream);
|
return readString(stream);
|
||||||
}
|
}
|
||||||
@@ -110,6 +113,9 @@ public final class IoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] readBytes(URL url) throws IOException {
|
public static byte[] readBytes(URL url) throws IOException {
|
||||||
|
if (HttpUtils.isHttpUrl(url)) {
|
||||||
|
throw new IllegalArgumentException("Should use HTTP client to GET " + url);
|
||||||
|
}
|
||||||
try (var stream = url.openStream()) {
|
try (var stream = url.openStream()) {
|
||||||
return stream.readAllBytes();
|
return stream.readAllBytes();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -940,6 +940,31 @@ ioErrorMakingHttpGet=\
|
|||||||
Exception when making request `GET {0}`:\n\
|
Exception when making request `GET {0}`:\n\
|
||||||
{1}
|
{1}
|
||||||
|
|
||||||
|
errorConnectingToHost=\
|
||||||
|
Error connecting to host `{0}`.
|
||||||
|
|
||||||
|
errorSslHandshake=\
|
||||||
|
Error during SSL handshake with host `{0}`:\n\
|
||||||
|
{1}
|
||||||
|
|
||||||
|
cannotInitHttpClient=\
|
||||||
|
Error initializing HTTP client:\n\
|
||||||
|
{0}
|
||||||
|
|
||||||
|
cannotFindCertFile=\
|
||||||
|
Cannot find CA certificate file `{0}`.
|
||||||
|
|
||||||
|
cannotReadCertFile=\
|
||||||
|
Error reading CA certificate file `{0}`:\n\
|
||||||
|
{1}
|
||||||
|
|
||||||
|
cannotParseCertFile=\
|
||||||
|
Error parsing CA certificate file `{0}`:\n\
|
||||||
|
{1}
|
||||||
|
|
||||||
|
emptyCertFile=\
|
||||||
|
CA certificate file `{0}` is empty.
|
||||||
|
|
||||||
invalidPackageZipUrl=\
|
invalidPackageZipUrl=\
|
||||||
Expected the zip asset for package `{0}` to be an HTTPS URI, but got `{1}`.
|
Expected the zip asset for package `{0}` to be an HTTPS URI, but got `{1}`.
|
||||||
|
|
||||||
@@ -1019,3 +1044,11 @@ The only supported checksum algorithm is sha256.
|
|||||||
|
|
||||||
testsFailed=\
|
testsFailed=\
|
||||||
Tests failed.
|
Tests failed.
|
||||||
|
|
||||||
|
expectedJarOrFileUrl=\
|
||||||
|
Certificates can only be loaded from `jar:` or `file:` URLs, but got:\n\
|
||||||
|
{0}
|
||||||
|
|
||||||
|
cannotFindBuiltInCertificates=\
|
||||||
|
Cannot find Pkl's trusted CA certificates on the class path.\n\
|
||||||
|
To fix this problem, add dependendy `org.pkl:pkl-certs`.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.pkl.core
|
|||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.pkl.commons.toPath
|
import org.pkl.commons.toPath
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.module.ModuleKeyFactories
|
import org.pkl.core.module.ModuleKeyFactories
|
||||||
import org.pkl.core.repl.ReplRequest
|
import org.pkl.core.repl.ReplRequest
|
||||||
import org.pkl.core.repl.ReplResponse
|
import org.pkl.core.repl.ReplResponse
|
||||||
@@ -12,6 +13,7 @@ import org.pkl.core.resource.ResourceReaders
|
|||||||
class ReplServerTest {
|
class ReplServerTest {
|
||||||
private val server = ReplServer(
|
private val server = ReplServer(
|
||||||
SecurityManagers.defaultManager,
|
SecurityManagers.defaultManager,
|
||||||
|
HttpClient.dummyClient(),
|
||||||
Loggers.stdErr(),
|
Loggers.stdErr(),
|
||||||
listOf(
|
listOf(
|
||||||
ModuleKeyFactories.standardLibrary,
|
ModuleKeyFactories.standardLibrary,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.pkl.core.http
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
|
||||||
|
class DummyHttpClientTest {
|
||||||
|
@Test
|
||||||
|
fun `refuses to send messages`() {
|
||||||
|
val client = HttpClient.dummyClient()
|
||||||
|
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
|
||||||
|
|
||||||
|
assertThrows<AssertionError> {
|
||||||
|
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows<AssertionError> {
|
||||||
|
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can be closed`() {
|
||||||
|
val client = HttpClient.dummyClient()
|
||||||
|
|
||||||
|
assertDoesNotThrow {
|
||||||
|
client.close()
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
148
pkl-core/src/test/kotlin/org/pkl/core/http/HttpClientTest.kt
Normal file
148
pkl-core/src/test/kotlin/org/pkl/core/http/HttpClientTest.kt
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package org.pkl.core.http
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import org.junit.jupiter.api.io.TempDir
|
||||||
|
import org.pkl.commons.test.FileTestUtils
|
||||||
|
import org.pkl.core.Release
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.time.Duration
|
||||||
|
import kotlin.io.path.copyTo
|
||||||
|
import kotlin.io.path.createDirectories
|
||||||
|
import kotlin.io.path.createFile
|
||||||
|
|
||||||
|
class HttpClientTest {
|
||||||
|
@Test
|
||||||
|
fun `can build default client`() {
|
||||||
|
val client = assertDoesNotThrow {
|
||||||
|
HttpClient.builder().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(client).isInstanceOf(RequestRewritingClient::class.java)
|
||||||
|
client as RequestRewritingClient
|
||||||
|
|
||||||
|
val release = Release.current()
|
||||||
|
assertThat(client.userAgent).isEqualTo("Pkl/${release.version()} (${release.os()}; ${release.flavor()})")
|
||||||
|
assertThat(client.requestTimeout).isEqualTo(Duration.ofSeconds(60))
|
||||||
|
|
||||||
|
assertThat(client.delegate).isInstanceOf(JdkHttpClient::class.java)
|
||||||
|
val delegate = client.delegate as JdkHttpClient
|
||||||
|
|
||||||
|
assertThat(delegate.underlying.connectTimeout()).hasValue(Duration.ofSeconds(60))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can build custom client`() {
|
||||||
|
val client = HttpClient.builder()
|
||||||
|
.setUserAgent("Agent 1")
|
||||||
|
.setRequestTimeout(Duration.ofHours(86))
|
||||||
|
.setConnectTimeout(Duration.ofMinutes(42))
|
||||||
|
.build() as RequestRewritingClient
|
||||||
|
|
||||||
|
assertThat(client.userAgent).isEqualTo("Agent 1")
|
||||||
|
assertThat(client.requestTimeout).isEqualTo(Duration.ofHours(86))
|
||||||
|
|
||||||
|
val delegate = client.delegate as JdkHttpClient
|
||||||
|
assertThat(delegate.underlying.connectTimeout()).hasValue(Duration.ofMinutes(42))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can load certificates from file system`() {
|
||||||
|
assertDoesNotThrow {
|
||||||
|
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `certificate file located on file system cannot be empty`(@TempDir tempDir: Path) {
|
||||||
|
val file = tempDir.resolve("certs.pem").createFile()
|
||||||
|
|
||||||
|
val e = assertThrows<HttpClientInitException> {
|
||||||
|
HttpClient.builder().addCertificates(file).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
fun `can load built-in certificates`() {
|
||||||
|
assertDoesNotThrow {
|
||||||
|
HttpClient.builder().addBuiltInCertificates().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can load certificates from Pkl user home cacerts directory`(@TempDir tempDir: Path) {
|
||||||
|
val certFile = tempDir.resolve(".pkl")
|
||||||
|
.resolve("cacerts")
|
||||||
|
.createDirectories()
|
||||||
|
.resolve("certs.pem")
|
||||||
|
FileTestUtils.selfSignedCertificate.copyTo(certFile)
|
||||||
|
|
||||||
|
assertDoesNotThrow {
|
||||||
|
HttpClientBuilder(tempDir).addDefaultCliCertificates().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loading certificates from cacerts directory falls back to built-in certificates`(@TempDir userHome: Path) {
|
||||||
|
assertDoesNotThrow {
|
||||||
|
HttpClientBuilder(userHome).addDefaultCliCertificates().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can be closed multiple times`() {
|
||||||
|
val client = HttpClient.builder().build()
|
||||||
|
|
||||||
|
assertDoesNotThrow {
|
||||||
|
client.close()
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `refuses to send messages once closed`() {
|
||||||
|
val client = HttpClient.builder().build()
|
||||||
|
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
assertThrows<IllegalStateException> {
|
||||||
|
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||||
|
}
|
||||||
|
assertThrows<IllegalStateException> {
|
||||||
|
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.pkl.core.http
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse.BodyHandlers
|
||||||
|
|
||||||
|
class LazyHttpClientTest {
|
||||||
|
@Test
|
||||||
|
fun `builds underlying client on first send`() {
|
||||||
|
val client = HttpClient.builder()
|
||||||
|
.addCertificates(javaClass.getResource("brokenCerts.pem")!!.toURI())
|
||||||
|
.buildLazily()
|
||||||
|
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
|
||||||
|
|
||||||
|
assertThrows<HttpClientInitException> {
|
||||||
|
client.send(request, BodyHandlers.discarding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `does not build underlying client unnecessarily`() {
|
||||||
|
val client = HttpClient.builder()
|
||||||
|
.addCertificates(javaClass.getResource("brokenCerts.pem")!!.toURI())
|
||||||
|
.buildLazily()
|
||||||
|
|
||||||
|
assertDoesNotThrow {
|
||||||
|
client.close()
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.pkl.core.http
|
||||||
|
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
|
||||||
|
class RequestCapturingClient : HttpClient {
|
||||||
|
lateinit var request: HttpRequest
|
||||||
|
|
||||||
|
override fun <T : Any?> send(
|
||||||
|
request: HttpRequest,
|
||||||
|
responseBodyHandler: HttpResponse.BodyHandler<T>
|
||||||
|
): HttpResponse<T>? {
|
||||||
|
this.request = request
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package org.pkl.core.http
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.assertThatList
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpRequest.BodyPublishers
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
import java.net.http.HttpResponse.BodyHandlers
|
||||||
|
import java.time.Duration
|
||||||
|
import java.net.http.HttpClient as JdkHttpClient
|
||||||
|
|
||||||
|
class RequestRewritingClientTest {
|
||||||
|
private val captured = RequestCapturingClient()
|
||||||
|
private val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), captured)
|
||||||
|
private val exampleUri = URI("https://example.com/foo/bar.html")
|
||||||
|
private val exampleRequest = HttpRequest.newBuilder(exampleUri).build()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fills in missing User-Agent header`() {
|
||||||
|
client.send(exampleRequest, BodyHandlers.discarding())
|
||||||
|
|
||||||
|
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("Pkl")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `overrides existing User-Agent headers`() {
|
||||||
|
val request = HttpRequest.newBuilder(exampleUri)
|
||||||
|
.header("User-Agent", "Agent 1")
|
||||||
|
.header("User-Agent", "Agent 2")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.send(request, BodyHandlers.discarding())
|
||||||
|
|
||||||
|
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("Pkl")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fills in missing request timeout`() {
|
||||||
|
client.send(exampleRequest, BodyHandlers.discarding())
|
||||||
|
|
||||||
|
assertThat(captured.request.timeout()).hasValue(Duration.ofSeconds(42))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `leaves existing request timeout intact`() {
|
||||||
|
val request = HttpRequest.newBuilder(exampleUri)
|
||||||
|
.timeout(Duration.ofMinutes(33))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.send(request, BodyHandlers.discarding())
|
||||||
|
|
||||||
|
assertThat(captured.request.timeout()).hasValue(Duration.ofMinutes(33))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fills in missing HTTP version`() {
|
||||||
|
client.send(exampleRequest, BodyHandlers.discarding())
|
||||||
|
|
||||||
|
assertThat(captured.request.version()).hasValue(JdkHttpClient.Version.HTTP_2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `leaves existing HTTP version intact`() {
|
||||||
|
val request = HttpRequest.newBuilder(exampleUri)
|
||||||
|
.version(JdkHttpClient.Version.HTTP_1_1)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.send(request, BodyHandlers.discarding())
|
||||||
|
|
||||||
|
assertThat(captured.request.version()).hasValue(JdkHttpClient.Version.HTTP_1_1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `leaves default method intact`() {
|
||||||
|
val request = HttpRequest.newBuilder(exampleUri).build()
|
||||||
|
|
||||||
|
client.send(request, BodyHandlers.discarding())
|
||||||
|
|
||||||
|
assertThat(captured.request.method()).isEqualTo("GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `leaves explicit method intact`() {
|
||||||
|
val request = HttpRequest.newBuilder(exampleUri)
|
||||||
|
.DELETE()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.send(request, BodyHandlers.discarding())
|
||||||
|
|
||||||
|
assertThat(captured.request.method()).isEqualTo("DELETE")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `leaves body publisher intact`() {
|
||||||
|
val publisher = BodyPublishers.ofString("body")
|
||||||
|
val request = HttpRequest.newBuilder(exampleUri)
|
||||||
|
.PUT(publisher)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.send(request, BodyHandlers.discarding())
|
||||||
|
|
||||||
|
assertThat(captured.request.bodyPublisher().get()).isSameAs(publisher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -13,9 +13,9 @@ import org.pkl.commons.readString
|
|||||||
import org.pkl.commons.test.FileTestUtils
|
import org.pkl.commons.test.FileTestUtils
|
||||||
import org.pkl.commons.test.PackageServer
|
import org.pkl.commons.test.PackageServer
|
||||||
import org.pkl.commons.test.listFilesRecursively
|
import org.pkl.commons.test.listFilesRecursively
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.SecurityManagers
|
import org.pkl.core.SecurityManagers
|
||||||
import org.pkl.core.module.PathElement
|
import org.pkl.core.module.PathElement
|
||||||
import org.pkl.core.runtime.CertificateUtils
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@@ -34,9 +34,12 @@ class PackageResolversTest {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
fun beforeAll() {
|
fun beforeAll() {
|
||||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
|
||||||
PackageServer.ensureStarted()
|
PackageServer.ensureStarted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val httpClient: HttpClient = HttpClient.builder()
|
||||||
|
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -196,16 +199,17 @@ class PackageResolversTest {
|
|||||||
@BeforeAll
|
@BeforeAll
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun beforeAll() {
|
fun beforeAll() {
|
||||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
|
||||||
PackageServer.ensureStarted()
|
PackageServer.ensureStarted()
|
||||||
cacheDir.deleteRecursively()
|
cacheDir.deleteRecursively()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val resolver: PackageResolver = PackageResolvers.DiskCachedPackageResolver(SecurityManagers.defaultManager, cacheDir)
|
override val resolver: PackageResolver = PackageResolvers.DiskCachedPackageResolver(
|
||||||
|
SecurityManagers.defaultManager, httpClient, cacheDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
class InMemoryPackageResolverTest : AbstractPackageResolverTest() {
|
class InMemoryPackageResolverTest : AbstractPackageResolverTest() {
|
||||||
override val resolver: PackageResolver = PackageResolvers.InMemoryPackageResolver(SecurityManagers.defaultManager)
|
override val resolver: PackageResolver = PackageResolvers.InMemoryPackageResolver(
|
||||||
|
SecurityManagers.defaultManager, httpClient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import org.pkl.commons.test.FileTestUtils
|
import org.pkl.commons.test.FileTestUtils
|
||||||
import org.pkl.commons.test.PackageServer
|
import org.pkl.commons.test.PackageServer
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.PklException
|
import org.pkl.core.PklException
|
||||||
import org.pkl.core.SecurityManagers
|
import org.pkl.core.SecurityManagers
|
||||||
import org.pkl.core.packages.PackageResolver
|
import org.pkl.core.packages.PackageResolver
|
||||||
import org.pkl.core.runtime.CertificateUtils
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
@@ -19,16 +19,19 @@ class ProjectDependenciesResolverTest {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
fun beforeAll() {
|
fun beforeAll() {
|
||||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
|
||||||
PackageServer.ensureStarted()
|
PackageServer.ensureStarted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val httpClient: HttpClient = HttpClient.builder()
|
||||||
|
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun resolveDependencies() {
|
fun resolveDependencies() {
|
||||||
val project2Path = Path.of(javaClass.getResource("project2/PklProject")!!.path)
|
val project2Path = Path.of(javaClass.getResource("project2/PklProject")!!.path)
|
||||||
val project = Project.loadFromPath(project2Path)
|
val project = Project.loadFromPath(project2Path)
|
||||||
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, null)
|
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null)
|
||||||
val deps = ProjectDependenciesResolver(project, packageResolver, System.out.writer()).resolve()
|
val deps = ProjectDependenciesResolver(project, packageResolver, System.out.writer()).resolve()
|
||||||
val strDeps = ByteArrayOutputStream()
|
val strDeps = ByteArrayOutputStream()
|
||||||
.apply { deps.writeTo(this) }
|
.apply { deps.writeTo(this) }
|
||||||
@@ -66,7 +69,7 @@ class ProjectDependenciesResolverTest {
|
|||||||
fun `fails if project declares a package with an incorrect checksum`() {
|
fun `fails if project declares a package with an incorrect checksum`() {
|
||||||
val projectPath = Path.of(javaClass.getResource("badProjectChecksum/PklProject")!!.path)
|
val projectPath = Path.of(javaClass.getResource("badProjectChecksum/PklProject")!!.path)
|
||||||
val project = Project.loadFromPath(projectPath)
|
val project = Project.loadFromPath(projectPath)
|
||||||
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, null)
|
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null)
|
||||||
val e = assertThrows<PklException> {
|
val e = assertThrows<PklException> {
|
||||||
ProjectDependenciesResolver(project, packageResolver, System.err.writer()).resolve()
|
ProjectDependenciesResolver(project, packageResolver, System.err.writer()).resolve()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import org.assertj.core.api.Assertions.assertThat
|
|||||||
import org.assertj.core.api.Assertions.assertThatCode
|
import org.assertj.core.api.Assertions.assertThatCode
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
|
import org.pkl.commons.test.FileTestUtils
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
@@ -137,9 +139,13 @@ class ProjectTest {
|
|||||||
PackageServer.ensureStarted()
|
PackageServer.ensureStarted()
|
||||||
val projectDir = Path.of(javaClass.getResource("badProjectChecksum2/")!!.path)
|
val projectDir = Path.of(javaClass.getResource("badProjectChecksum2/")!!.path)
|
||||||
val project = Project.loadFromPath(projectDir.resolve("PklProject"))
|
val project = Project.loadFromPath(projectDir.resolve("PklProject"))
|
||||||
|
val httpClient = HttpClient.builder()
|
||||||
|
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||||
|
.build()
|
||||||
val evaluator = EvaluatorBuilder.preconfigured()
|
val evaluator = EvaluatorBuilder.preconfigured()
|
||||||
.applyFromProject(project)
|
.applyFromProject(project)
|
||||||
.setModuleCacheDir(null)
|
.setModuleCacheDir(null)
|
||||||
|
.setHttpClient(httpClient)
|
||||||
.build()
|
.build()
|
||||||
assertThatCode { evaluator.evaluate(ModuleSource.path(projectDir.resolve("bug.pkl"))) }
|
assertThatCode { evaluator.evaluate(ModuleSource.path(projectDir.resolve("bug.pkl"))) }
|
||||||
.hasMessageStartingWith("""
|
.hasMessageStartingWith("""
|
||||||
|
|||||||
48
pkl-core/src/test/kotlin/org/pkl/core/util/ExceptionsTest.kt
Normal file
48
pkl-core/src/test/kotlin/org/pkl/core/util/ExceptionsTest.kt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package org.pkl.core.util
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.io.IOException
|
||||||
|
import java.lang.Error
|
||||||
|
|
||||||
|
class ExceptionsTest {
|
||||||
|
@Test
|
||||||
|
fun `get root cause of simple exception`() {
|
||||||
|
val e = IOException("io")
|
||||||
|
assertThat(Exceptions.getRootCause(e)).isSameAs(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get root cause of nested exception`() {
|
||||||
|
val e = IOException("io")
|
||||||
|
val e2 = RuntimeException("runtime")
|
||||||
|
val e3 = Error("error")
|
||||||
|
e.initCause(e2)
|
||||||
|
e2.initCause(e3)
|
||||||
|
assertThat(Exceptions.getRootCause(e)).isSameAs(e3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get root reason`() {
|
||||||
|
val e = IOException("io")
|
||||||
|
val e2 = RuntimeException("the root reason")
|
||||||
|
e.initCause(e2)
|
||||||
|
assertThat(Exceptions.getRootReason(e)).isEqualTo("the root reason")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get root reason if null`() {
|
||||||
|
val e = IOException("io")
|
||||||
|
val e2 = RuntimeException(null as String?)
|
||||||
|
e.initCause(e2)
|
||||||
|
assertThat(Exceptions.getRootReason(e)).isEqualTo("(unknown reason)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get root reason if empty`() {
|
||||||
|
val e = IOException("io")
|
||||||
|
val e2 = RuntimeException("")
|
||||||
|
e.initCause(e2)
|
||||||
|
assertThat(Exceptions.getRootReason(e)).isEqualTo("(unknown reason)")
|
||||||
|
}
|
||||||
|
}
|
||||||
42
pkl-core/src/test/kotlin/org/pkl/core/util/HttpUtilsTest.kt
Normal file
42
pkl-core/src/test/kotlin/org/pkl/core/util/HttpUtilsTest.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package org.pkl.core.util
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import org.pkl.commons.test.FakeHttpResponse
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class HttpUtilsTest {
|
||||||
|
@Test
|
||||||
|
fun isHttpUrl() {
|
||||||
|
assertThat(HttpUtils.isHttpUrl(URI("http://example.com"))).isTrue
|
||||||
|
assertThat(HttpUtils.isHttpUrl(URI("https://example.com"))).isTrue
|
||||||
|
assertThat(HttpUtils.isHttpUrl(URI("HtTpS://example.com"))).isTrue
|
||||||
|
assertThat(HttpUtils.isHttpUrl(URI("file://example.com"))).isFalse
|
||||||
|
|
||||||
|
assertThat(HttpUtils.isHttpUrl(URL("http://example.com"))).isTrue
|
||||||
|
assertThat(HttpUtils.isHttpUrl(URL("https://example.com"))).isTrue
|
||||||
|
assertThat(HttpUtils.isHttpUrl(URL("HtTpS://example.com"))).isTrue
|
||||||
|
assertThat(HttpUtils.isHttpUrl(URL("file://example.com"))).isFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkHasStatusCode200() {
|
||||||
|
val response = FakeHttpResponse.withoutBody {
|
||||||
|
statusCode = 200
|
||||||
|
}
|
||||||
|
assertDoesNotThrow {
|
||||||
|
HttpUtils.checkHasStatusCode200(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response2 = FakeHttpResponse.withoutBody {
|
||||||
|
statusCode = 404
|
||||||
|
}
|
||||||
|
assertThrows<IOException> {
|
||||||
|
HttpUtils.checkHasStatusCode200(response2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import org.pkl.core.runtime.ModuleResolver
|
|||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.io.path.createFile
|
import kotlin.io.path.createFile
|
||||||
|
|
||||||
@@ -410,4 +411,24 @@ class IoUtilsTest {
|
|||||||
IoUtils.resolve(FakeSecurityManager, key2, URI("...NamedModuleResolversTest.pkl"))
|
IoUtils.resolve(FakeSecurityManager, key2, URI("...NamedModuleResolversTest.pkl"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `readBytes(URL) does not support HTTP URLs`() {
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
IoUtils.readBytes(URL("https://example.com"))
|
||||||
|
}
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
IoUtils.readBytes(URL("http://example.com"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `readString(URL) does not support HTTP URLs`() {
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
IoUtils.readString(URL("https://example.com"))
|
||||||
|
}
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
IoUtils.readString(URL("http://example.com"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
broken
|
||||||
@@ -35,7 +35,8 @@ import org.pkl.core.packages.*
|
|||||||
*/
|
*/
|
||||||
class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(options.base) {
|
class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(options.base) {
|
||||||
|
|
||||||
private val packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir)
|
private val packageResolver =
|
||||||
|
PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir)
|
||||||
|
|
||||||
private val stdlibDependency =
|
private val stdlibDependency =
|
||||||
DocPackageInfo.PackageDependency(
|
DocPackageInfo.PackageDependency(
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ plugins {
|
|||||||
pklKotlinTest
|
pklKotlinTest
|
||||||
}
|
}
|
||||||
|
|
||||||
val pklDistribution: Configuration by configurations.creating
|
val pklDistributionCurrent: Configuration by configurations.creating
|
||||||
|
val pklDistribution025: Configuration by configurations.creating
|
||||||
|
|
||||||
// Because pkl-executor doesn't depend on other Pkl modules
|
// Because pkl-executor doesn't depend on other Pkl modules
|
||||||
// (nor has overlapping dependencies that could cause a version conflict),
|
// (nor has overlapping dependencies that could cause a version conflict),
|
||||||
// clients are free to use different versions of pkl-executor and (say) pkl-config-java-all.
|
// clients are free to use different versions of pkl-executor and (say) pkl-config-java-all.
|
||||||
// (Pkl distributions used by EmbeddedExecutor are isolated via class loaders.)
|
// (Pkl distributions used by EmbeddedExecutor are isolated via class loaders.)
|
||||||
dependencies {
|
dependencies {
|
||||||
pklDistribution(project(":pkl-config-java", "fatJar"))
|
pklDistributionCurrent(project(":pkl-config-java", "fatJar"))
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
pklDistribution025(libs.pklConfigJavaAll025)
|
||||||
|
|
||||||
implementation(libs.slf4jApi)
|
implementation(libs.slf4jApi)
|
||||||
|
|
||||||
@@ -49,7 +52,14 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
// this task could be folded into tasks.test by switching to IntelliJ's Gradle test runner
|
||||||
|
val prepareTest by tasks.registering {
|
||||||
// used by EmbeddedExecutorTest
|
// used by EmbeddedExecutorTest
|
||||||
dependsOn(pklDistribution)
|
dependsOn(pklDistributionCurrent, pklDistribution025)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
dependsOn(prepareTest)
|
||||||
|
systemProperty("pklDistributionCurrent", pklDistributionCurrent.singleFile)
|
||||||
|
systemProperty("pklDistribution025", pklDistribution025.singleFile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import java.util.regex.Pattern;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.pkl.executor.spi.v1.ExecutorSpi;
|
import org.pkl.executor.spi.v1.ExecutorSpi;
|
||||||
import org.pkl.executor.spi.v1.ExecutorSpiException;
|
import org.pkl.executor.spi.v1.ExecutorSpiException;
|
||||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -40,12 +39,17 @@ final class EmbeddedExecutor implements Executor {
|
|||||||
private final List<PklDistribution> pklDistributions = new ArrayList<>();
|
private final List<PklDistribution> pklDistributions = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws IllegalArgumentException if a Jar file cannot be found or is not a valid PklPkl
|
* @throws IllegalArgumentException if a Jar file cannot be found or is not a valid Pkl
|
||||||
* distribution
|
* distribution
|
||||||
*/
|
*/
|
||||||
public EmbeddedExecutor(List<Path> pklFatJars) {
|
public EmbeddedExecutor(List<Path> pklFatJars) {
|
||||||
|
this(pklFatJars, Executor.class.getClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
// for testing only
|
||||||
|
EmbeddedExecutor(List<Path> pklFatJars, ClassLoader pklExecutorClassLoader) {
|
||||||
for (var jarFile : pklFatJars) {
|
for (var jarFile : pklFatJars) {
|
||||||
pklDistributions.add(new PklDistribution(jarFile));
|
pklDistributions.add(new PklDistribution(jarFile, pklExecutorClassLoader));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +76,7 @@ final class EmbeddedExecutor implements Executor {
|
|||||||
// (but not any modules imported by it) and only requires parsing (but not evaluating) the
|
// (but not any modules imported by it) and only requires parsing (but not evaluating) the
|
||||||
// module.
|
// module.
|
||||||
requestedVersion = detectRequestedPklVersion(modulePath, options);
|
requestedVersion = detectRequestedPklVersion(modulePath, options);
|
||||||
|
//noinspection resource
|
||||||
distribution = findCompatibleDistribution(modulePath, requestedVersion, options);
|
distribution = findCompatibleDistribution(modulePath, requestedVersion, options);
|
||||||
output = distribution.evaluatePath(modulePath, options);
|
output = distribution.evaluatePath(modulePath, options);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
@@ -163,22 +168,23 @@ final class EmbeddedExecutor implements Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class PklDistribution implements AutoCloseable {
|
private static final class PklDistribution implements AutoCloseable {
|
||||||
final URLClassLoader classLoader;
|
final URLClassLoader pklDistributionClassLoader;
|
||||||
final ExecutorSpi executorSpi;
|
final /* @Nullable */ ExecutorSpi executorSpi;
|
||||||
final Version version;
|
final Version version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws IllegalArgumentException if the Jar file does not exist or is not a valid Pkl
|
* @throws IllegalArgumentException if the Jar file does not exist or is not a valid Pkl
|
||||||
* distribution
|
* distribution
|
||||||
*/
|
*/
|
||||||
PklDistribution(Path pklFatJar) {
|
PklDistribution(Path pklFatJar, ClassLoader pklExecutorClassLoader) {
|
||||||
if (!Files.isRegularFile(pklFatJar)) {
|
if (!Files.isRegularFile(pklFatJar)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
String.format("Invalid Pkl distribution: Cannot find Jar file `%s`.", pklFatJar));
|
String.format("Invalid Pkl distribution: Cannot find Jar file `%s`.", pklFatJar));
|
||||||
}
|
}
|
||||||
|
|
||||||
classLoader = new PklDistributionClassLoader(pklFatJar);
|
pklDistributionClassLoader =
|
||||||
var serviceLoader = ServiceLoader.load(ExecutorSpi.class, classLoader);
|
new PklDistributionClassLoader(pklFatJar, pklExecutorClassLoader);
|
||||||
|
var serviceLoader = ServiceLoader.load(ExecutorSpi.class, pklDistributionClassLoader);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
executorSpi = serviceLoader.iterator().next();
|
executorSpi = serviceLoader.iterator().next();
|
||||||
@@ -208,9 +214,9 @@ final class EmbeddedExecutor implements Executor {
|
|||||||
var currentThread = Thread.currentThread();
|
var currentThread = Thread.currentThread();
|
||||||
var prevContextClassLoader = currentThread.getContextClassLoader();
|
var prevContextClassLoader = currentThread.getContextClassLoader();
|
||||||
// Truffle loads stuff from context class loader, so set it to our class loader
|
// Truffle loads stuff from context class loader, so set it to our class loader
|
||||||
currentThread.setContextClassLoader(classLoader);
|
currentThread.setContextClassLoader(pklDistributionClassLoader);
|
||||||
try {
|
try {
|
||||||
return executorSpi.evaluatePath(modulePath, toEvaluatorOptions(options));
|
return executorSpi.evaluatePath(modulePath, options.toSpiOptions());
|
||||||
} catch (ExecutorSpiException e) {
|
} catch (ExecutorSpiException e) {
|
||||||
throw new ExecutorException(e.getMessage(), e.getCause());
|
throw new ExecutorException(e.getMessage(), e.getCause());
|
||||||
} finally {
|
} finally {
|
||||||
@@ -220,30 +226,21 @@ final class EmbeddedExecutor implements Executor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
classLoader.close();
|
pklDistributionClassLoader.close();
|
||||||
}
|
|
||||||
|
|
||||||
ExecutorSpiOptions toEvaluatorOptions(ExecutorOptions options) {
|
|
||||||
return new ExecutorSpiOptions(
|
|
||||||
options.getAllowedModules(),
|
|
||||||
options.getAllowedResources(),
|
|
||||||
options.getEnvironmentVariables(),
|
|
||||||
options.getExternalProperties(),
|
|
||||||
options.getModulePath(),
|
|
||||||
options.getRootDir(),
|
|
||||||
options.getTimeout(),
|
|
||||||
options.getOutputFormat(),
|
|
||||||
options.getModuleCacheDir(),
|
|
||||||
options.getProjectDir());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class PklDistributionClassLoader extends URLClassLoader {
|
private static final class PklDistributionClassLoader extends URLClassLoader {
|
||||||
final ClassLoader spiClassLoader = ExecutorSpi.class.getClassLoader();
|
final ClassLoader pklExecutorClassLoader;
|
||||||
|
|
||||||
PklDistributionClassLoader(Path pklFatJar) {
|
static {
|
||||||
|
registerAsParallelCapable();
|
||||||
|
}
|
||||||
|
|
||||||
|
PklDistributionClassLoader(Path pklFatJar, ClassLoader pklExecutorClassLoader) {
|
||||||
// pass `null` to make bootstrap class loader the effective parent
|
// pass `null` to make bootstrap class loader the effective parent
|
||||||
super(toUrls(pklFatJar), null);
|
super(toUrls(pklFatJar), null);
|
||||||
|
this.pklExecutorClassLoader = pklExecutorClassLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -253,7 +250,15 @@ final class EmbeddedExecutor implements Executor {
|
|||||||
|
|
||||||
if (clazz == null) {
|
if (clazz == null) {
|
||||||
if (name.startsWith("org.pkl.executor.spi.")) {
|
if (name.startsWith("org.pkl.executor.spi.")) {
|
||||||
clazz = spiClassLoader.loadClass(name);
|
try {
|
||||||
|
// give pkl-executor a chance to load the SPI clasa
|
||||||
|
clazz = pklExecutorClassLoader.loadClass(name);
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
// The SPI class exists in this distribution but not in pkl-executor,
|
||||||
|
// so load it from the distribution.
|
||||||
|
// This can happen if the pkl-executor version is lower than the distribution version.
|
||||||
|
clazz = findClass(name);
|
||||||
|
}
|
||||||
} else if (name.startsWith("java.")
|
} else if (name.startsWith("java.")
|
||||||
|| name.startsWith("jdk.")
|
|| name.startsWith("jdk.")
|
||||||
|| name.startsWith("sun.")
|
|| name.startsWith("sun.")
|
||||||
@@ -282,18 +287,14 @@ final class EmbeddedExecutor implements Executor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URL getResource(String name) {
|
public URL getResource(String name) {
|
||||||
// try bootstrap class loader first
|
var resource = getPlatformClassLoader().getResource(name);
|
||||||
// once we move to JDK 9+, should use `getPlatformClassLoader().getResource()` instead of
|
|
||||||
// `super.getResource()`
|
|
||||||
var resource = super.getResource(name);
|
|
||||||
return resource != null ? resource : findResource(name);
|
return resource != null ? resource : findResource(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Enumeration<URL> getResources(String name) throws IOException {
|
public Enumeration<URL> getResources(String name) throws IOException {
|
||||||
// once we move to JDK 9+, should use `getPlatformClassLoader().getResources()` instead of
|
return ConcatenatedEnumeration.create(
|
||||||
// `super.getResources()`
|
getPlatformClassLoader().getResources(name), findResources(name));
|
||||||
return ConcatenatedEnumeration.create(super.getResources(name), findResources(name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static URL[] toUrls(Path pklFatJar) {
|
static URL[] toUrls(Path pklFatJar) {
|
||||||
|
|||||||
@@ -20,27 +20,33 @@ import java.time.Duration;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
|
||||||
|
|
||||||
/** Options for {@link Executor#evaluatePath}. */
|
/**
|
||||||
public final class ExecutorOptions {
|
* Options for {@link Executor#evaluatePath}.
|
||||||
private final List<String> allowedModules;
|
*
|
||||||
|
* <p>Note that subclasses of {@code ExecutorOptions} offer additional options.
|
||||||
|
*/
|
||||||
|
public class ExecutorOptions {
|
||||||
|
protected final List<String> allowedModules;
|
||||||
|
|
||||||
private final List<String> allowedResources;
|
protected final List<String> allowedResources;
|
||||||
|
|
||||||
private final Map<String, String> environmentVariables;
|
protected final Map<String, String> environmentVariables;
|
||||||
|
|
||||||
private final Map<String, String> externalProperties;
|
protected final Map<String, String> externalProperties;
|
||||||
|
|
||||||
private final List<Path> modulePath;
|
protected final List<Path> modulePath;
|
||||||
|
|
||||||
private final /* @Nullable */ Path rootDir;
|
protected final /* @Nullable */ Path rootDir;
|
||||||
|
|
||||||
private final /* @Nullable */ Duration timeout;
|
protected final /* @Nullable */ Duration timeout;
|
||||||
|
|
||||||
private final /* @Nullable */ String outputFormat;
|
protected final /* @Nullable */ String outputFormat;
|
||||||
|
|
||||||
private final /* @Nullable */ Path moduleCacheDir;
|
protected final /* @Nullable */ Path moduleCacheDir;
|
||||||
private final /* @Nullable */ Path projectDir;
|
|
||||||
|
protected final /* @Nullable */ Path projectDir;
|
||||||
|
|
||||||
/** Returns the module cache dir that the CLI uses by default. */
|
/** Returns the module cache dir that the CLI uses by default. */
|
||||||
public static Path defaultModuleCacheDir() {
|
public static Path defaultModuleCacheDir() {
|
||||||
@@ -148,7 +154,7 @@ public final class ExecutorOptions {
|
|||||||
@Override
|
@Override
|
||||||
public boolean equals(/* @Nullable */ Object obj) {
|
public boolean equals(/* @Nullable */ Object obj) {
|
||||||
if (this == obj) return true;
|
if (this == obj) return true;
|
||||||
if (!(obj instanceof ExecutorOptions)) return false;
|
if (obj.getClass() != ExecutorOptions.class) return false;
|
||||||
|
|
||||||
var other = (ExecutorOptions) obj;
|
var other = (ExecutorOptions) obj;
|
||||||
return allowedModules.equals(other.allowedModules)
|
return allowedModules.equals(other.allowedModules)
|
||||||
@@ -203,4 +209,18 @@ public final class ExecutorOptions {
|
|||||||
+ projectDir
|
+ projectDir
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExecutorSpiOptions toSpiOptions() {
|
||||||
|
return new ExecutorSpiOptions(
|
||||||
|
allowedModules,
|
||||||
|
allowedResources,
|
||||||
|
environmentVariables,
|
||||||
|
externalProperties,
|
||||||
|
modulePath,
|
||||||
|
rootDir,
|
||||||
|
timeout,
|
||||||
|
outputFormat,
|
||||||
|
moduleCacheDir,
|
||||||
|
projectDir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.executor;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for {@link Executor#evaluatePath}.
|
||||||
|
*
|
||||||
|
* <p>This class offers additional options not available in {@code ExecutorOptions}.
|
||||||
|
*/
|
||||||
|
public class ExecutorOptions2 extends ExecutorOptions {
|
||||||
|
protected final List<Path> certificateFiles;
|
||||||
|
protected final List<URI> certificateUris;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an options object.
|
||||||
|
*
|
||||||
|
* @param allowedModules API equivalent of the {@code --allowed-modules} CLI option
|
||||||
|
* @param allowedResources API equivalent of the {@code --allowed-resources} CLI option
|
||||||
|
* @param environmentVariables API equivalent of the repeatable {@code --env-var} CLI option
|
||||||
|
* @param externalProperties API equivalent of the repeatable {@code --property} CLI option
|
||||||
|
* @param modulePath API equivalent of the {@code --module-path} CLI option
|
||||||
|
* @param rootDir API equivalent of the {@code --root-dir} CLI option
|
||||||
|
* @param timeout API equivalent of the {@code --timeout} CLI option
|
||||||
|
* @param outputFormat API equivalent of the {@code --format} CLI option
|
||||||
|
* @param moduleCacheDir API equivalent of the {@code --cache-dir} CLI option. Passing {@link
|
||||||
|
* #defaultModuleCacheDir()} is equivalent to omitting {@code --cache-dir}. Passing {@code
|
||||||
|
* null} is equivalent to {@code --no-cache}.
|
||||||
|
* @param projectDir API equivalent of the {@code --project-dir} CLI option.
|
||||||
|
* @param certificateFiles API equivalent of the {@code --ca-certificates} CLI option
|
||||||
|
* @param certificateUris API equivalent of the {@code --ca-certificates} CLI option
|
||||||
|
*/
|
||||||
|
public ExecutorOptions2(
|
||||||
|
List<String> allowedModules,
|
||||||
|
List<String> allowedResources,
|
||||||
|
Map<String, String> environmentVariables,
|
||||||
|
Map<String, String> externalProperties,
|
||||||
|
List<Path> modulePath,
|
||||||
|
/* @Nullable */ Path rootDir,
|
||||||
|
/* @Nullable */ Duration timeout,
|
||||||
|
/* @Nullable */ String outputFormat,
|
||||||
|
/* @Nullable */ Path moduleCacheDir,
|
||||||
|
/* @Nullable */ Path projectDir,
|
||||||
|
List<Path> certificateFiles,
|
||||||
|
List<URI> certificateUris) {
|
||||||
|
|
||||||
|
super(
|
||||||
|
allowedModules,
|
||||||
|
allowedResources,
|
||||||
|
environmentVariables,
|
||||||
|
externalProperties,
|
||||||
|
modulePath,
|
||||||
|
rootDir,
|
||||||
|
timeout,
|
||||||
|
outputFormat,
|
||||||
|
moduleCacheDir,
|
||||||
|
projectDir);
|
||||||
|
this.certificateFiles = certificateFiles;
|
||||||
|
this.certificateUris = certificateUris;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API equivalent of the {@code --ca-certificates} CLI option. */
|
||||||
|
public List<Path> getCertificateFiles() {
|
||||||
|
return certificateFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API equivalent of the {@code --ca-certificates} CLI option. */
|
||||||
|
public List<URI> getCertificateUris() {
|
||||||
|
return certificateUris;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(/* @Nullable */ Object obj) {
|
||||||
|
if (this == obj) return true;
|
||||||
|
if (obj.getClass() != ExecutorOptions2.class) return false;
|
||||||
|
|
||||||
|
var other = (ExecutorOptions2) obj;
|
||||||
|
return allowedModules.equals(other.allowedModules)
|
||||||
|
&& allowedResources.equals(other.allowedResources)
|
||||||
|
&& environmentVariables.equals(other.environmentVariables)
|
||||||
|
&& externalProperties.equals(other.externalProperties)
|
||||||
|
&& modulePath.equals(other.modulePath)
|
||||||
|
&& Objects.equals(rootDir, other.rootDir)
|
||||||
|
&& Objects.equals(timeout, other.timeout)
|
||||||
|
&& Objects.equals(outputFormat, other.outputFormat)
|
||||||
|
&& Objects.equals(moduleCacheDir, other.moduleCacheDir)
|
||||||
|
&& Objects.equals(projectDir, other.projectDir)
|
||||||
|
&& Objects.equals(certificateFiles, other.certificateFiles)
|
||||||
|
&& Objects.equals(certificateUris, other.certificateUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(
|
||||||
|
allowedModules,
|
||||||
|
allowedResources,
|
||||||
|
environmentVariables,
|
||||||
|
externalProperties,
|
||||||
|
modulePath,
|
||||||
|
rootDir,
|
||||||
|
timeout,
|
||||||
|
outputFormat,
|
||||||
|
moduleCacheDir,
|
||||||
|
projectDir,
|
||||||
|
certificateFiles,
|
||||||
|
certificateUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ExecutorOptions2{"
|
||||||
|
+ "allowedModules="
|
||||||
|
+ allowedModules
|
||||||
|
+ ", allowedResources="
|
||||||
|
+ allowedResources
|
||||||
|
+ ", environmentVariables="
|
||||||
|
+ environmentVariables
|
||||||
|
+ ", externalProperties="
|
||||||
|
+ externalProperties
|
||||||
|
+ ", modulePath="
|
||||||
|
+ modulePath
|
||||||
|
+ ", rootDir="
|
||||||
|
+ rootDir
|
||||||
|
+ ", timeout="
|
||||||
|
+ timeout
|
||||||
|
+ ", outputFormat="
|
||||||
|
+ outputFormat
|
||||||
|
+ ", cacheDir="
|
||||||
|
+ moduleCacheDir
|
||||||
|
+ ", projectDir="
|
||||||
|
+ projectDir
|
||||||
|
+ ", certificateFiles="
|
||||||
|
+ certificateFiles
|
||||||
|
+ ", certificateUris="
|
||||||
|
+ certificateUris
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecutorSpiOptions2 toSpiOptions() {
|
||||||
|
return new ExecutorSpiOptions2(
|
||||||
|
allowedModules,
|
||||||
|
allowedResources,
|
||||||
|
environmentVariables,
|
||||||
|
externalProperties,
|
||||||
|
modulePath,
|
||||||
|
rootDir,
|
||||||
|
timeout,
|
||||||
|
outputFormat,
|
||||||
|
moduleCacheDir,
|
||||||
|
projectDir,
|
||||||
|
certificateFiles,
|
||||||
|
certificateUris);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.executor.spi.v1;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ExecutorSpiOptions2 extends ExecutorSpiOptions {
|
||||||
|
private final List<Path> certificateFiles;
|
||||||
|
|
||||||
|
private final List<URI> certificateUris;
|
||||||
|
|
||||||
|
public ExecutorSpiOptions2(
|
||||||
|
List<String> allowedModules,
|
||||||
|
List<String> allowedResources,
|
||||||
|
Map<String, String> environmentVariables,
|
||||||
|
Map<String, String> externalProperties,
|
||||||
|
List<Path> modulePath,
|
||||||
|
Path rootDir,
|
||||||
|
Duration timeout,
|
||||||
|
String outputFormat,
|
||||||
|
Path moduleCacheDir,
|
||||||
|
Path projectDir,
|
||||||
|
List<Path> certificateFiles,
|
||||||
|
List<URI> certificateUris) {
|
||||||
|
super(
|
||||||
|
allowedModules,
|
||||||
|
allowedResources,
|
||||||
|
environmentVariables,
|
||||||
|
externalProperties,
|
||||||
|
modulePath,
|
||||||
|
rootDir,
|
||||||
|
timeout,
|
||||||
|
outputFormat,
|
||||||
|
moduleCacheDir,
|
||||||
|
projectDir);
|
||||||
|
this.certificateFiles = certificateFiles;
|
||||||
|
this.certificateUris = certificateUris;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Path> getCertificateFiles() {
|
||||||
|
return certificateFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<URI> getCertificateUris() {
|
||||||
|
return certificateUris;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,159 @@
|
|||||||
package org.pkl.executor
|
package org.pkl.executor
|
||||||
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.AfterAll
|
||||||
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.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
import org.pkl.commons.test.FileTestUtils
|
import org.pkl.commons.test.FileTestUtils
|
||||||
|
import org.pkl.commons.test.FilteringClassLoader
|
||||||
import org.pkl.commons.test.PackageServer
|
import org.pkl.commons.test.PackageServer
|
||||||
import org.pkl.commons.toPath
|
import org.pkl.commons.toPath
|
||||||
import org.pkl.commons.walk
|
import org.pkl.core.Release
|
||||||
import org.pkl.core.runtime.CertificateUtils
|
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import kotlin.io.path.createDirectories
|
import kotlin.io.path.createDirectories
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
|
||||||
class EmbeddedExecutorTest {
|
class EmbeddedExecutorTest {
|
||||||
private val pklDistribution by lazy {
|
/**
|
||||||
val libsDir = FileTestUtils.rootProjectDir.resolve("pkl-config-java/build/libs")
|
* A combination of ExecutorOptions version, pkl-executor version,
|
||||||
if (!Files.isDirectory(libsDir)) {
|
* and Pkl distribution version that parameterized tests should be run against.
|
||||||
throw AssertionError(
|
*/
|
||||||
"JAR `pkl-config-java-all` does not exist. Run `./gradlew :pkl-config-java:build` to create it."
|
data class ExecutionContext(
|
||||||
|
val executor: Executor,
|
||||||
|
val options: (ExecutorOptions) -> ExecutorOptions,
|
||||||
|
val name: String
|
||||||
|
) {
|
||||||
|
override fun toString(): String = name
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
private val allExecutionContexts: List<ExecutionContext> by lazy {
|
||||||
|
listOf(
|
||||||
|
ExecutionContext(executor1_1.value, ::convertToOptions1, "Options1, Executor1, Distribution1"),
|
||||||
|
|
||||||
|
// This context has a pkl-executor version that is lower than the distribution version.
|
||||||
|
// It can be enabled once there is a distribution that includes pkl-executor.
|
||||||
|
//ExecutionContext(executor1_2.value, ::convertToOptions1, "Options1, Executor1, Distribution2"),
|
||||||
|
|
||||||
|
ExecutionContext(executor2_1.value, ::convertToOptions1, "Options1, Executor2, Distribution1"),
|
||||||
|
ExecutionContext(executor2_1.value, ::convertToOptions2, "Options2, Executor2, Distribution1"),
|
||||||
|
|
||||||
|
ExecutionContext(executor2_2.value, ::convertToOptions1, "Options1, Executor2, Distribution2"),
|
||||||
|
ExecutionContext(executor2_2.value, ::convertToOptions2, "Options2, Executor2, Distribution2")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
libsDir.walk()
|
|
||||||
.filter { path ->
|
private val currentExecutor: Executor by lazy { executor2_2.value }
|
||||||
path.toString().let {
|
|
||||||
it.contains("-all") &&
|
// A pkl-executor library that supports ExecutorSpiOptions up to v1
|
||||||
it.endsWith(".jar") &&
|
// and a Pkl distribution that supports ExecutorSpiOptions up to v1.
|
||||||
!it.contains("-sources") &&
|
private val executor1_1: Lazy<Executor> = lazy {
|
||||||
!it.contains("-javadoc")
|
EmbeddedExecutor(listOf(pklDistribution1), pklExecutorClassLoader1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pkl-executor library that supports ExecutorSpiOptions up to v1
|
||||||
|
// and a Pkl distribution that supports ExecutorSpiOptions up to v2.
|
||||||
|
private val executor1_2: Lazy<Executor> = lazy {
|
||||||
|
EmbeddedExecutor(listOf(pklDistribution2), pklExecutorClassLoader1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pkl-executor library that supports ExecutorSpiOptions up to v2
|
||||||
|
// and a Pkl distribution that supports ExecutorSpiOptions up to v1.
|
||||||
|
private val executor2_1: Lazy<Executor> = lazy {
|
||||||
|
EmbeddedExecutor(listOf(pklDistribution1), pklExecutorClassLoader2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pkl-executor library that supports ExecutorSpiOptions up to v2
|
||||||
|
// and a Pkl distribution that supports ExecutorSpiOptions up to v.
|
||||||
|
private val executor2_2: Lazy<Executor> = lazy {
|
||||||
|
EmbeddedExecutor(listOf(pklDistribution2), pklExecutorClassLoader2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val allExecutors by lazy {
|
||||||
|
listOf(executor1_1, executor1_2, executor2_1, executor2_2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// a pkl-executor class loader that supports ExecutorSpiOptions up to v1
|
||||||
|
private val pklExecutorClassLoader1: ClassLoader by lazy {
|
||||||
|
FilteringClassLoader(pklExecutorClassLoader2) { className ->
|
||||||
|
!className.endsWith("ExecutorSpiOptions2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a pkl-executor class loader that supports ExecutorSpiOptions up to v2
|
||||||
|
private val pklExecutorClassLoader2: ClassLoader by lazy {
|
||||||
|
EmbeddedExecutor::class.java.classLoader
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
@JvmStatic
|
||||||
|
fun afterAll() {
|
||||||
|
for (executor in allExecutors) {
|
||||||
|
if (executor.isInitialized()) executor.value.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a Pkl distribution that supports ExecutorSpiOptions up to v1
|
||||||
|
private val pklDistribution1: Path by lazy {
|
||||||
|
val path = System.getProperty("pklDistribution025")?.toPath() ?:
|
||||||
|
// can get rid of this path by switching to IntelliJ's Gradle test runner
|
||||||
|
System.getProperty("user.home").toPath()
|
||||||
|
.resolve(".gradle/caches/modules-2/files-2.1/org.pkl-lang/pkl-config-java-all/" +
|
||||||
|
"0.25.0/e9451dda554f1659e49ff5bdd30accd26be7bf0f/pkl-config-java-all-0.25.0.jar")
|
||||||
|
path.apply {
|
||||||
|
if (!exists()) throw AssertionError("Missing test fixture. " +
|
||||||
|
"To fix this problem, run `./gradlew :pkl-executor:prepareTest`.")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a Pkl distribution that supports ExecutorSpiOptions up to v2
|
||||||
|
private val pklDistribution2: Path by lazy {
|
||||||
|
val path = System.getProperty("pklDistributionCurrent")?.toPath() ?:
|
||||||
|
// can get rid of this path by switching to IntelliJ's Gradle test runner
|
||||||
|
FileTestUtils.rootProjectDir
|
||||||
|
.resolve("pkl-config-java/build/libs/pkl-config-java-all-" +
|
||||||
|
"${Release.current().version().withBuild(null).toString().replaceFirst("dev", "SNAPSHOT")}.jar")
|
||||||
|
path.apply {
|
||||||
|
if (!exists()) throw AssertionError("Missing test fixture. " +
|
||||||
|
"To fix this problem, run `./gradlew :pkl-executor:prepareTest`.")
|
||||||
}
|
}
|
||||||
.findFirst()
|
}
|
||||||
.orElseThrow {
|
|
||||||
AssertionError(
|
private fun convertToOptions2(options: ExecutorOptions): ExecutorOptions2 =
|
||||||
"JAR `pkl-config-java-all` does not exist. Run `./gradlew :pkl-config-java:build` to create it."
|
if (options is ExecutorOptions2) options else ExecutorOptions2(
|
||||||
)
|
options.allowedModules,
|
||||||
}
|
options.allowedResources,
|
||||||
|
options.environmentVariables,
|
||||||
|
options.externalProperties,
|
||||||
|
options.modulePath,
|
||||||
|
options.rootDir,
|
||||||
|
options.timeout,
|
||||||
|
options.outputFormat,
|
||||||
|
options.moduleCacheDir,
|
||||||
|
options.projectDir,
|
||||||
|
listOf(),
|
||||||
|
listOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun convertToOptions1(options: ExecutorOptions): ExecutorOptions =
|
||||||
|
if (options.javaClass == ExecutorOptions::class.java) options else ExecutorOptions(
|
||||||
|
options.allowedModules,
|
||||||
|
options.allowedResources,
|
||||||
|
options.environmentVariables,
|
||||||
|
options.externalProperties,
|
||||||
|
options.modulePath,
|
||||||
|
options.rootDir,
|
||||||
|
options.timeout,
|
||||||
|
options.outputFormat,
|
||||||
|
options.moduleCacheDir,
|
||||||
|
options.projectDir
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -121,8 +238,9 @@ class EmbeddedExecutorTest {
|
|||||||
.contains("pkl.jar")
|
.contains("pkl.jar")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
fun `evaluate a module that is missing a ModuleInfo annotation`(@TempDir tempDir: Path) {
|
@MethodSource("getAllExecutionContexts")
|
||||||
|
fun `evaluate a module that is missing a ModuleInfo annotation`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||||
val pklFile = tempDir.resolve("test.pkl")
|
val pklFile = tempDir.resolve("test.pkl")
|
||||||
pklFile.toFile().writeText(
|
pklFile.toFile().writeText(
|
||||||
"""
|
"""
|
||||||
@@ -132,12 +250,10 @@ class EmbeddedExecutorTest {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|
||||||
val executor = Executors.embedded(listOf(pklDistribution))
|
|
||||||
val e = assertThrows<ExecutorException> {
|
val e = assertThrows<ExecutorException> {
|
||||||
executor.use {
|
context.executor.evaluatePath(
|
||||||
it.evaluatePath(
|
|
||||||
pklFile,
|
pklFile,
|
||||||
ExecutorOptions(
|
context.options(ExecutorOptions2(
|
||||||
listOf("file:"),
|
listOf("file:"),
|
||||||
listOf("prop:"),
|
listOf("prop:"),
|
||||||
mapOf(),
|
mapOf(),
|
||||||
@@ -147,18 +263,20 @@ class EmbeddedExecutorTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
listOf(),
|
||||||
|
listOf()
|
||||||
)
|
)
|
||||||
)
|
))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(e.message)
|
assertThat(e.message)
|
||||||
.contains("Pkl module `test.pkl` does not state which Pkl version it requires.")
|
.contains("Pkl module `test.pkl` does not state which Pkl version it requires.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
fun `evaluate a module that requests an incompatible Pkl version`(@TempDir tempDir: Path) {
|
@MethodSource("getAllExecutionContexts")
|
||||||
|
fun `evaluate a module that requests an incompatible Pkl version`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||||
val pklFile = tempDir.resolve("test.pkl")
|
val pklFile = tempDir.resolve("test.pkl")
|
||||||
pklFile.toFile().writeText(
|
pklFile.toFile().writeText(
|
||||||
"""
|
"""
|
||||||
@@ -169,12 +287,10 @@ class EmbeddedExecutorTest {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|
||||||
val executor = Executors.embedded(listOf(pklDistribution))
|
|
||||||
val e = assertThrows<ExecutorException> {
|
val e = assertThrows<ExecutorException> {
|
||||||
executor.use {
|
context.executor.evaluatePath(
|
||||||
it.evaluatePath(
|
|
||||||
pklFile,
|
pklFile,
|
||||||
ExecutorOptions(
|
context.options(ExecutorOptions2(
|
||||||
listOf("file:"),
|
listOf("file:"),
|
||||||
listOf("prop:"),
|
listOf("prop:"),
|
||||||
mapOf(),
|
mapOf(),
|
||||||
@@ -184,18 +300,20 @@ class EmbeddedExecutorTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
)
|
listOf(),
|
||||||
|
listOf()
|
||||||
|
))
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(e.message)
|
assertThat(e.message)
|
||||||
.contains("Pkl version `99.99.99` requested by module `test.pkl` is not supported.")
|
.contains("Pkl version `99.99.99` requested by module `test.pkl` is not supported.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
fun `evaluate a module that reads environment variables and external properties`(@TempDir tempDir: Path) {
|
@MethodSource("getAllExecutionContexts")
|
||||||
|
fun `evaluate a module that reads environment variables and external properties`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||||
val pklFile = tempDir.resolve("test.pkl")
|
val pklFile = tempDir.resolve("test.pkl")
|
||||||
pklFile.toFile().writeText(
|
pklFile.toFile().writeText(
|
||||||
"""
|
"""
|
||||||
@@ -207,11 +325,9 @@ class EmbeddedExecutorTest {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|
||||||
val executor = Executors.embedded(listOf(pklDistribution))
|
val result = context.executor.evaluatePath(
|
||||||
val result = executor.use {
|
|
||||||
it.evaluatePath(
|
|
||||||
pklFile,
|
pklFile,
|
||||||
ExecutorOptions(
|
context.options(ExecutorOptions2(
|
||||||
listOf("file:"),
|
listOf("file:"),
|
||||||
// should `prop:pkl.outputFormat` be allowed automatically?
|
// should `prop:pkl.outputFormat` be allowed automatically?
|
||||||
listOf("prop:", "env:"),
|
listOf("prop:", "env:"),
|
||||||
@@ -222,10 +338,11 @@ class EmbeddedExecutorTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
)
|
listOf(),
|
||||||
|
listOf()
|
||||||
|
))
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(result.trim()).isEqualTo(
|
assertThat(result.trim()).isEqualTo(
|
||||||
"""
|
"""
|
||||||
@@ -235,8 +352,9 @@ class EmbeddedExecutorTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
fun `evaluate a module that depends on another module`(@TempDir tempDir: Path) {
|
@MethodSource("getAllExecutionContexts")
|
||||||
|
fun `evaluate a module that depends on another module`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||||
val pklFile = tempDir.resolve("test.pkl")
|
val pklFile = tempDir.resolve("test.pkl")
|
||||||
pklFile.toFile().writeText(
|
pklFile.toFile().writeText(
|
||||||
"""
|
"""
|
||||||
@@ -260,11 +378,9 @@ class EmbeddedExecutorTest {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|
||||||
val executor = Executors.embedded(listOf(pklDistribution))
|
val result = context.executor.evaluatePath(
|
||||||
val result = executor.use {
|
|
||||||
it.evaluatePath(
|
|
||||||
pklFile,
|
pklFile,
|
||||||
ExecutorOptions(
|
context.options(ExecutorOptions2(
|
||||||
listOf("file:"),
|
listOf("file:"),
|
||||||
listOf("prop:"),
|
listOf("prop:"),
|
||||||
mapOf(),
|
mapOf(),
|
||||||
@@ -274,10 +390,12 @@ class EmbeddedExecutorTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
listOf(),
|
||||||
|
listOf()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
assertThat(result.trim()).isEqualTo(
|
assertThat(result.trim()).isEqualTo(
|
||||||
"""
|
"""
|
||||||
@@ -288,8 +406,9 @@ class EmbeddedExecutorTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
fun `evaluate a module whose evaluation fails`(@TempDir tempDir: Path) {
|
@MethodSource("getAllExecutionContexts")
|
||||||
|
fun `evaluate a module whose evaluation fails`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||||
val pklFile = tempDir.resolve("test.pkl")
|
val pklFile = tempDir.resolve("test.pkl")
|
||||||
pklFile.toFile().writeText(
|
pklFile.toFile().writeText(
|
||||||
"""
|
"""
|
||||||
@@ -300,12 +419,10 @@ class EmbeddedExecutorTest {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|
||||||
val executor = Executors.embedded(listOf(pklDistribution))
|
|
||||||
val e = assertThrows<ExecutorException> {
|
val e = assertThrows<ExecutorException> {
|
||||||
executor.use {
|
context.executor.evaluatePath(
|
||||||
it.evaluatePath(
|
|
||||||
pklFile,
|
pklFile,
|
||||||
ExecutorOptions(
|
context.options(ExecutorOptions2(
|
||||||
listOf("file:"),
|
listOf("file:"),
|
||||||
listOf("prop:"),
|
listOf("prop:"),
|
||||||
mapOf(),
|
mapOf(),
|
||||||
@@ -315,10 +432,11 @@ class EmbeddedExecutorTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
listOf(),
|
||||||
|
listOf()
|
||||||
)
|
)
|
||||||
)
|
))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(e.message)
|
assertThat(e.message)
|
||||||
@@ -328,8 +446,9 @@ class EmbeddedExecutorTest {
|
|||||||
.doesNotContain(tempDir.toString())
|
.doesNotContain(tempDir.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
fun `time out a module`(@TempDir tempDir: Path) {
|
@MethodSource("getAllExecutionContexts")
|
||||||
|
fun `time out a module`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||||
val pklFile = tempDir.resolve("test.pkl")
|
val pklFile = tempDir.resolve("test.pkl")
|
||||||
pklFile.toFile().writeText(
|
pklFile.toFile().writeText(
|
||||||
"""
|
"""
|
||||||
@@ -342,12 +461,10 @@ class EmbeddedExecutorTest {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
|
|
||||||
val executor = Executors.embedded(listOf(pklDistribution))
|
|
||||||
val e = assertThrows<ExecutorException> {
|
val e = assertThrows<ExecutorException> {
|
||||||
executor.use {
|
context.executor.evaluatePath(
|
||||||
it.evaluatePath(
|
|
||||||
pklFile,
|
pklFile,
|
||||||
ExecutorOptions(
|
context.options(ExecutorOptions2(
|
||||||
listOf("file:"),
|
listOf("file:"),
|
||||||
listOf("prop:"),
|
listOf("prop:"),
|
||||||
mapOf(),
|
mapOf(),
|
||||||
@@ -357,59 +474,20 @@ class EmbeddedExecutorTest {
|
|||||||
Duration.ofSeconds(1),
|
Duration.ofSeconds(1),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
)
|
listOf(),
|
||||||
|
listOf()
|
||||||
|
))
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(e.message)
|
assertThat(e.message)
|
||||||
.contains("Evaluation timed out after 1 second(s).")
|
.contains("Evaluation timed out after 1 second(s).")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only packages are cached.
|
|
||||||
// Because this test doesn't import a package, it doesn't really test
|
|
||||||
// that the `moduleCacheDir` option takes effect.
|
|
||||||
@Test
|
|
||||||
fun `evaluate a module with enabled module cache`(@TempDir tempDir: Path) {
|
|
||||||
val pklFile = tempDir.resolve("test.pkl")
|
|
||||||
pklFile.toFile().writeText(
|
|
||||||
"""
|
|
||||||
@ModuleInfo { minPklVersion = "0.16.0" }
|
|
||||||
module test
|
|
||||||
|
|
||||||
x = 42
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
|
|
||||||
val executor = Executors.embedded(listOf(pklDistribution))
|
|
||||||
val result = executor.use {
|
|
||||||
it.evaluatePath(
|
|
||||||
pklFile,
|
|
||||||
ExecutorOptions(
|
|
||||||
listOf("file:"),
|
|
||||||
listOf("prop:"),
|
|
||||||
mapOf(),
|
|
||||||
mapOf(),
|
|
||||||
listOf(),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
ExecutorOptions.defaultModuleCacheDir(),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(result.trim()).isEqualTo(
|
|
||||||
"""
|
|
||||||
x = 42
|
|
||||||
""".trimIndent().trim()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `evaluate a module that loads a package`(@TempDir tempDir: Path) {
|
fun `evaluate a module that loads a package`(@TempDir tempDir: Path) {
|
||||||
|
val cacheDir = tempDir.resolve("cache")
|
||||||
val pklFile = tempDir.resolve("test.pkl")
|
val pklFile = tempDir.resolve("test.pkl")
|
||||||
pklFile.toFile().writeText(
|
pklFile.toFile().writeText(
|
||||||
"""
|
"""
|
||||||
@@ -422,11 +500,8 @@ class EmbeddedExecutorTest {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
PackageServer.ensureStarted()
|
PackageServer.ensureStarted()
|
||||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
val result = currentExecutor.evaluatePath(pklFile,
|
||||||
val executor = Executors.embedded(listOf(pklDistribution))
|
ExecutorOptions2(
|
||||||
val result = executor.use {
|
|
||||||
it.evaluatePath(pklFile,
|
|
||||||
ExecutorOptions(
|
|
||||||
listOf("file:", "package:", "https:"),
|
listOf("file:", "package:", "https:"),
|
||||||
listOf("prop:", "package:", "https:"),
|
listOf("prop:", "package:", "https:"),
|
||||||
mapOf(),
|
mapOf(),
|
||||||
@@ -435,10 +510,11 @@ class EmbeddedExecutorTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
ExecutorOptions.defaultModuleCacheDir(),
|
cacheDir,
|
||||||
null)
|
null,
|
||||||
|
listOf(FileTestUtils.selfSignedCertificate),
|
||||||
|
listOf())
|
||||||
)
|
)
|
||||||
}
|
|
||||||
assertThat(result.trim()).isEqualTo("""
|
assertThat(result.trim()).isEqualTo("""
|
||||||
chirpy {
|
chirpy {
|
||||||
name = "Chirpy"
|
name = "Chirpy"
|
||||||
@@ -447,10 +523,14 @@ class EmbeddedExecutorTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
|
|
||||||
|
// verify that cache was populated
|
||||||
|
assertThat(cacheDir.toFile().list()).isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
fun `evaluate a project dependency`(@TempDir tempDir: Path) {
|
@MethodSource("getAllExecutionContexts")
|
||||||
|
fun `evaluate a project dependency`(context: ExecutionContext, @TempDir tempDir: Path) {
|
||||||
val cacheDir = tempDir.resolve("packages")
|
val cacheDir = tempDir.resolve("packages")
|
||||||
PackageServer.populateCacheDir(cacheDir)
|
PackageServer.populateCacheDir(cacheDir)
|
||||||
val projectDir = tempDir.resolve("project/")
|
val projectDir = tempDir.resolve("project/")
|
||||||
@@ -495,10 +575,8 @@ class EmbeddedExecutorTest {
|
|||||||
result = Swallow
|
result = Swallow
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
val executor = Executors.embedded(listOf(pklDistribution))
|
val result = context.executor.evaluatePath(pklFile,
|
||||||
val result = executor.use {
|
context.options(ExecutorOptions2(
|
||||||
it.evaluatePath(pklFile,
|
|
||||||
ExecutorOptions(
|
|
||||||
listOf("file:", "package:", "projectpackage:", "https:"),
|
listOf("file:", "package:", "projectpackage:", "https:"),
|
||||||
listOf("prop:", "package:", "projectpackage:", "https:"),
|
listOf("prop:", "package:", "projectpackage:", "https:"),
|
||||||
mapOf(),
|
mapOf(),
|
||||||
@@ -508,9 +586,10 @@ class EmbeddedExecutorTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
cacheDir,
|
cacheDir,
|
||||||
projectDir)
|
projectDir,
|
||||||
)
|
listOf(),
|
||||||
}
|
listOf())
|
||||||
|
))
|
||||||
assertThat(result).isEqualTo("""
|
assertThat(result).isEqualTo("""
|
||||||
result {
|
result {
|
||||||
name = "Swallow"
|
name = "Swallow"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import java.time.Duration
|
|||||||
import org.msgpack.core.MessagePacker
|
import org.msgpack.core.MessagePacker
|
||||||
import org.pkl.core.*
|
import org.pkl.core.*
|
||||||
import org.pkl.core.ast.member.ObjectMember
|
import org.pkl.core.ast.member.ObjectMember
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.module.ModuleKeyFactory
|
import org.pkl.core.module.ModuleKeyFactory
|
||||||
import org.pkl.core.project.DeclaredDependencies
|
import org.pkl.core.project.DeclaredDependencies
|
||||||
import org.pkl.core.resource.ResourceReader
|
import org.pkl.core.resource.ResourceReader
|
||||||
@@ -28,6 +29,7 @@ import org.pkl.core.runtime.*
|
|||||||
internal class BinaryEvaluator(
|
internal class BinaryEvaluator(
|
||||||
transformer: StackFrameTransformer,
|
transformer: StackFrameTransformer,
|
||||||
manager: SecurityManager,
|
manager: SecurityManager,
|
||||||
|
httpClient: HttpClient,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
factories: Collection<ModuleKeyFactory?>,
|
factories: Collection<ModuleKeyFactory?>,
|
||||||
readers: Collection<ResourceReader?>,
|
readers: Collection<ResourceReader?>,
|
||||||
@@ -41,6 +43,7 @@ internal class BinaryEvaluator(
|
|||||||
EvaluatorImpl(
|
EvaluatorImpl(
|
||||||
transformer,
|
transformer,
|
||||||
manager,
|
manager,
|
||||||
|
httpClient,
|
||||||
logger,
|
logger,
|
||||||
factories,
|
factories,
|
||||||
readers,
|
readers,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import java.util.concurrent.ExecutorService
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import org.pkl.core.*
|
import org.pkl.core.*
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.module.ModuleKeyFactories
|
import org.pkl.core.module.ModuleKeyFactories
|
||||||
import org.pkl.core.module.ModuleKeyFactory
|
import org.pkl.core.module.ModuleKeyFactory
|
||||||
import org.pkl.core.module.ModulePathResolver
|
import org.pkl.core.module.ModulePathResolver
|
||||||
@@ -29,7 +30,8 @@ import org.pkl.core.project.DeclaredDependencies
|
|||||||
import org.pkl.core.resource.ResourceReader
|
import org.pkl.core.resource.ResourceReader
|
||||||
import org.pkl.core.resource.ResourceReaders
|
import org.pkl.core.resource.ResourceReaders
|
||||||
|
|
||||||
class Server(private val transport: MessageTransport) : AutoCloseable {
|
class Server(private val transport: MessageTransport, private val httpClient: HttpClient) :
|
||||||
|
AutoCloseable {
|
||||||
private val evaluators: MutableMap<Long, BinaryEvaluator> = ConcurrentHashMap()
|
private val evaluators: MutableMap<Long, BinaryEvaluator> = ConcurrentHashMap()
|
||||||
|
|
||||||
// https://github.com/jano7/executor would be the perfect executor here
|
// https://github.com/jano7/executor would be the perfect executor here
|
||||||
@@ -173,6 +175,7 @@ class Server(private val transport: MessageTransport) : AutoCloseable {
|
|||||||
SecurityManagers.defaultTrustLevels,
|
SecurityManagers.defaultTrustLevels,
|
||||||
rootDir
|
rootDir
|
||||||
),
|
),
|
||||||
|
httpClient,
|
||||||
ClientLogger(evaluatorId, transport),
|
ClientLogger(evaluatorId, transport),
|
||||||
createModuleKeyFactories(message, evaluatorId, resolver),
|
createModuleKeyFactories(message, evaluatorId, resolver),
|
||||||
createResourceReaders(message, evaluatorId, resolver),
|
createResourceReaders(message, evaluatorId, resolver),
|
||||||
|
|||||||
@@ -16,13 +16,12 @@
|
|||||||
package org.pkl.server
|
package org.pkl.server
|
||||||
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import kotlin.Pair
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import org.junit.platform.commons.annotation.Testable
|
import org.junit.platform.commons.annotation.Testable
|
||||||
import org.pkl.commons.test.InputOutputTestEngine
|
import org.pkl.commons.test.InputOutputTestEngine
|
||||||
import org.pkl.core.Loggers
|
import org.pkl.core.*
|
||||||
import org.pkl.core.ModuleSource
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.SecurityManagers
|
|
||||||
import org.pkl.core.StackFrameTransformers
|
|
||||||
import org.pkl.core.module.ModuleKeyFactories
|
import org.pkl.core.module.ModuleKeyFactories
|
||||||
|
|
||||||
@Testable class BinaryEvaluatorSnippetTests
|
@Testable class BinaryEvaluatorSnippetTests
|
||||||
@@ -47,6 +46,7 @@ class BinaryEvaluatorSnippetTestEngine : InputOutputTestEngine() {
|
|||||||
BinaryEvaluator(
|
BinaryEvaluator(
|
||||||
StackFrameTransformers.empty,
|
StackFrameTransformers.empty,
|
||||||
SecurityManagers.defaultManager,
|
SecurityManagers.defaultManager,
|
||||||
|
HttpClient.dummyClient(),
|
||||||
Loggers.stdErr(),
|
Loggers.stdErr(),
|
||||||
listOf(ModuleKeyFactories.file),
|
listOf(ModuleKeyFactories.file),
|
||||||
listOf(),
|
listOf(),
|
||||||
|
|||||||
@@ -21,6 +21,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.pkl.core.*
|
import org.pkl.core.*
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.module.ModuleKeyFactories
|
import org.pkl.core.module.ModuleKeyFactories
|
||||||
import org.pkl.core.resource.ResourceReaders
|
import org.pkl.core.resource.ResourceReaders
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class BinaryEvaluatorTest {
|
|||||||
SecurityManagers.defaultTrustLevels,
|
SecurityManagers.defaultTrustLevels,
|
||||||
Path.of("")
|
Path.of("")
|
||||||
),
|
),
|
||||||
|
HttpClient.dummyClient(),
|
||||||
Loggers.noop(),
|
Loggers.noop(),
|
||||||
listOf(ModuleKeyFactories.standardLibrary),
|
listOf(ModuleKeyFactories.standardLibrary),
|
||||||
listOf(ResourceReaders.environmentVariable(), ResourceReaders.externalProperty()),
|
listOf(ResourceReaders.environmentVariable(), ResourceReaders.externalProperty()),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
import org.msgpack.core.MessagePack
|
import org.msgpack.core.MessagePack
|
||||||
import org.pkl.commons.test.PackageServer
|
import org.pkl.commons.test.PackageServer
|
||||||
|
import org.pkl.core.http.HttpClient
|
||||||
import org.pkl.core.module.PathElement
|
import org.pkl.core.module.PathElement
|
||||||
|
|
||||||
class ServerTest {
|
class ServerTest {
|
||||||
@@ -67,7 +68,7 @@ class ServerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val client: TestTransport = TestTransport(transports.first)
|
private val client: TestTransport = TestTransport(transports.first)
|
||||||
private val server: Server = Server(transports.second)
|
private val server: Server = Server(transports.second, HttpClient.dummyClient())
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun before() {
|
fun before() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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