mirror of
https://github.com/apple/pkl.git
synced 2026-05-25 16:19:20 +02:00
Improve HTTP headers logic (#1584)
* Relax forbidden headers constraints - remove restriction on browser-related headers - allow any glob pattern (no need to end with `/` or `*`, because glob patterns already require users to explicitly declare prefix matches if that's the intention) * Replace `List<Pair<, ...>>`; use `Map<String, ...>` instead * Use glob pattern strings as an API throughout, instead of `Pattern` (e.g. in `HttpClientBuilder`) * Add HTTP headers to message passing API * Add HTTP headers to executor API (introduces `ExecutorSpiOptions4`) * Add tests for Gradle, CLI, and pkl-executor invocations * Improve documentation * Add `isGlobPattern` API to class `String` for in-language validation of http headers * Behavior change: make sure explicitly configured `User-Agent` in `HttpClientBuilder` can be shadowed by headers (allows users to set `--http-header "**=User-Agent: My User Agent"` and for this to be the only user agent). CC @kyokuping
This commit is contained in:
@@ -39,6 +39,7 @@ dependencies {
|
||||
testImplementation(projects.pklCommonsTest)
|
||||
testImplementation(projects.pklCore)
|
||||
testImplementation(libs.slf4jSimple)
|
||||
testImplementation(libs.wiremock)
|
||||
}
|
||||
|
||||
// TODO why is this needed? Without this, we get error:
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions3;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions4;
|
||||
|
||||
/**
|
||||
* Options for {@link Executor#evaluatePath}.
|
||||
@@ -58,6 +59,8 @@ public final class ExecutorOptions {
|
||||
|
||||
private final Map<URI, URI> httpRewrites;
|
||||
|
||||
private final Map<String, Map<String, List<String>>> httpHeaders;
|
||||
|
||||
private final int testPort; // -1 means disabled
|
||||
|
||||
private final int spiOptionsVersion; // -1 means use latest
|
||||
@@ -92,6 +95,7 @@ public final class ExecutorOptions {
|
||||
private Map<URI, URI> httpRewrites = Map.of();
|
||||
private int testPort = -1; // -1 means disabled
|
||||
private int spiOptionsVersion = -1; // -1 means use latest
|
||||
private Map<String, Map<String, List<String>>> httpHeaders = Map.of();
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@@ -215,6 +219,18 @@ public final class ExecutorOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* API equivalent of the {@code --http-header} CLI option.
|
||||
*
|
||||
* <p>This option is ignored on Pkl 0.31 and older.
|
||||
*
|
||||
* @since 0.32.0
|
||||
*/
|
||||
public Builder httpHeaders(Map<String, Map<String, List<String>>> httpHeaders) {
|
||||
this.httpHeaders = httpHeaders;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Internal test option. -1 means disabled. */
|
||||
Builder testPort(int testPort) {
|
||||
this.testPort = testPort;
|
||||
@@ -242,6 +258,7 @@ public final class ExecutorOptions {
|
||||
certificateFiles,
|
||||
certificateBytes,
|
||||
httpRewrites,
|
||||
httpHeaders,
|
||||
testPort,
|
||||
spiOptionsVersion);
|
||||
}
|
||||
@@ -291,6 +308,7 @@ public final class ExecutorOptions {
|
||||
List.of(),
|
||||
List.of(),
|
||||
Map.of(),
|
||||
Map.of(),
|
||||
-1,
|
||||
-1);
|
||||
}
|
||||
@@ -309,6 +327,7 @@ public final class ExecutorOptions {
|
||||
List<Path> certificateFiles,
|
||||
List<byte[]> certificateBytes,
|
||||
Map<URI, URI> httpRewrites,
|
||||
Map<String, Map<String, List<String>>> httpHeaders,
|
||||
int testPort,
|
||||
int spiOptionsVersion) {
|
||||
|
||||
@@ -325,6 +344,7 @@ public final class ExecutorOptions {
|
||||
this.certificateFiles = List.copyOf(certificateFiles);
|
||||
this.certificateBytes = List.copyOf(certificateBytes);
|
||||
this.httpRewrites = Map.copyOf(httpRewrites);
|
||||
this.httpHeaders = Map.copyOf(httpHeaders);
|
||||
this.testPort = testPort;
|
||||
this.spiOptionsVersion = spiOptionsVersion;
|
||||
}
|
||||
@@ -425,6 +445,7 @@ public final class ExecutorOptions {
|
||||
&& Objects.equals(certificateFiles, other.certificateFiles)
|
||||
&& Objects.equals(certificateBytes, other.certificateBytes)
|
||||
&& Objects.equals(httpRewrites, other.httpRewrites)
|
||||
&& Objects.equals(httpHeaders, other.httpHeaders)
|
||||
&& testPort == other.testPort
|
||||
&& spiOptionsVersion == other.spiOptionsVersion;
|
||||
}
|
||||
@@ -445,6 +466,7 @@ public final class ExecutorOptions {
|
||||
certificateFiles,
|
||||
certificateBytes,
|
||||
httpRewrites,
|
||||
httpHeaders,
|
||||
testPort,
|
||||
spiOptionsVersion);
|
||||
}
|
||||
@@ -478,6 +500,8 @@ public final class ExecutorOptions {
|
||||
+ certificateBytes
|
||||
+ ", httpRewrites="
|
||||
+ httpRewrites
|
||||
+ ", httpHeaders="
|
||||
+ httpHeaders
|
||||
+ ", testPort="
|
||||
+ testPort
|
||||
+ ", spiOptionsVersion="
|
||||
@@ -487,7 +511,24 @@ public final class ExecutorOptions {
|
||||
|
||||
ExecutorSpiOptions toSpiOptions() {
|
||||
return switch (spiOptionsVersion) {
|
||||
case -1, 3 ->
|
||||
case -1, 4 ->
|
||||
new ExecutorSpiOptions4(
|
||||
allowedModules,
|
||||
allowedResources,
|
||||
environmentVariables,
|
||||
externalProperties,
|
||||
modulePath,
|
||||
rootDir,
|
||||
timeout,
|
||||
outputFormat,
|
||||
moduleCacheDir,
|
||||
projectDir,
|
||||
certificateFiles,
|
||||
certificateBytes,
|
||||
testPort,
|
||||
httpRewrites,
|
||||
httpHeaders);
|
||||
case 3 ->
|
||||
new ExecutorSpiOptions3(
|
||||
allowedModules,
|
||||
allowedResources,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright © 2026 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;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public class ExecutorSpiOptions4 extends ExecutorSpiOptions3 {
|
||||
|
||||
private final Map<String, Map<String, List<String>>> httpHeaders;
|
||||
|
||||
public ExecutorSpiOptions4(
|
||||
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<byte[]> certificateBytes,
|
||||
int testPort,
|
||||
Map<URI, URI> httpRewrites,
|
||||
Map<String, Map<String, List<String>>> httpHeaders) {
|
||||
super(
|
||||
allowedModules,
|
||||
allowedResources,
|
||||
environmentVariables,
|
||||
externalProperties,
|
||||
modulePath,
|
||||
rootDir,
|
||||
timeout,
|
||||
outputFormat,
|
||||
moduleCacheDir,
|
||||
projectDir,
|
||||
certificateFiles,
|
||||
certificateBytes,
|
||||
testPort,
|
||||
httpRewrites);
|
||||
this.httpHeaders = httpHeaders;
|
||||
}
|
||||
|
||||
public Map<String, Map<String, List<String>>> getHttpHeaders() {
|
||||
return httpHeaders;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,15 @@
|
||||
*/
|
||||
package org.pkl.executor
|
||||
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.equalTo
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.get
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.ok
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.stubFor
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.verify
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockTest
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
@@ -39,6 +48,7 @@ import org.pkl.commons.test.PackageServer
|
||||
import org.pkl.commons.toPath
|
||||
import org.pkl.core.Release
|
||||
|
||||
@WireMockTest
|
||||
class EmbeddedExecutorTest {
|
||||
/**
|
||||
* An executor that uses a particular combination of ExecutorSpiOptions version, pkl-executor
|
||||
@@ -139,7 +149,7 @@ class EmbeddedExecutorTest {
|
||||
.apply { if (!exists()) missingTestFixture() }
|
||||
}
|
||||
|
||||
// a Pkl distribution that supports ExecutorSpiOptions up to v3
|
||||
// a Pkl distribution that supports ExecutorSpiOptions up to v4
|
||||
private val pklDistribution2: Path by lazy {
|
||||
FileTestUtils.rootProjectDir
|
||||
.resolve(
|
||||
@@ -590,6 +600,30 @@ class EmbeddedExecutorTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `http headers option`(@TempDir tempDir: Path, wwRuntimeInfo: WireMockRuntimeInfo) {
|
||||
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(ok("foo = 1")))
|
||||
val pklFile =
|
||||
tempDir.resolve("test.pkl").also {
|
||||
it.writeText(
|
||||
"""
|
||||
@ModuleInfo { minPklVersion = "0.32.0" }
|
||||
module test
|
||||
|
||||
res = import("${wwRuntimeInfo.httpBaseUrl}/foo.pkl")
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
currentExecutor.evaluatePath(pklFile) {
|
||||
allowedModules("file:", "https:", "http:")
|
||||
allowedResources("prop:")
|
||||
httpHeaders(mapOf("**" to mapOf("X-Foo" to listOf("Foo"))))
|
||||
}
|
||||
verify(getRequestedFor(urlEqualTo("/foo.pkl")).withHeader("X-Foo", equalTo("Foo")))
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("getCompatibleTestExecutors")
|
||||
@DisabledOnOs(OS.WINDOWS, disabledReason = "Can't populate legacy cache dir on Windows")
|
||||
|
||||
Reference in New Issue
Block a user