mirror of
https://github.com/apple/pkl.git
synced 2026-03-30 13:51:57 +02:00
Add support for HTTP rewrites (#1062)
This adds a new configuration option for the HTTP client to replace URI prefixes when making outbound calls. Follows the design of https://github.com/apple/pkl-evolution/pull/17
This commit is contained in:
@@ -2154,6 +2154,9 @@ Optionally, the SHA-256 checksum of the package can also be specified:
|
||||
Packages can be managed as dependencies within a _project_.
|
||||
For more details, consult the <<projects,project>> section of the language reference.
|
||||
|
||||
Packages can also be downloaded from a mirror.
|
||||
For more details, consult the <<mirroring_packages>> section of the language reference.
|
||||
|
||||
==== Standard Library URI
|
||||
|
||||
Example: `+pkl:math+`
|
||||
@@ -3204,7 +3207,8 @@ This section discusses language features that are generally more relevant to tem
|
||||
<<reserved-keywords,Reserved Keywords>> +
|
||||
<<blank-identifiers,Blank Identifiers>> +
|
||||
<<projects,Projects>> +
|
||||
<<external-readers,External Readers>>
|
||||
<<external-readers,External Readers>> +
|
||||
<<mirroring_packages,Mirroring packages>>
|
||||
|
||||
[[meaning-of-new]]
|
||||
=== Meaning of `new`
|
||||
@@ -5755,3 +5759,25 @@ To support both schemes during evaluation, both would need to be registered expl
|
||||
----
|
||||
$ pkl eval <module> --external-resource-reader ldap=pkl-ldap --external-resource-reader ldaps=pkl-ldap
|
||||
----
|
||||
|
||||
[[mirroring_packages]]
|
||||
=== Mirroring packages
|
||||
|
||||
A package is a shareable archive of modules and resources that are published to the internet.
|
||||
|
||||
A package's URI tells two things:
|
||||
|
||||
1. The name of the package.
|
||||
2. Where the package is downloaded from.
|
||||
|
||||
For example, given the package name `package://example.com/mypackage@1.0.0`, Pkl will make an HTTPS request to `\https://example.com/mypackage@1.0.0` to fetch package metadata.
|
||||
|
||||
In situations where internet access is restricted, a mirror can be set up to allow use of packages that are published to the internet.
|
||||
|
||||
To direct Pkl to a mirror, the `--http-rewrite` CLI option (and its equivalent options when using Pkl's other evaluator APIs) must be used.
|
||||
For example, `--http-rewrite \https://pkg.pkl-lang.org/=\https://my.internal.mirror/` will tell Pkl to download packages from host `my.internal.mirror`.
|
||||
|
||||
NOTE: To effectively mirror packages from pkg.pkl-lang.org, there must be two rewrites; one for `\https://pkg.pkl-lang.org/` (where package metadata is downloaded), and one for `\https://github.com/` (where package zip files are downloaded).
|
||||
|
||||
NOTE: Pkl does not provide any tooling to run a mirror server.
|
||||
To fully set up mirroring, an HTTP(s) server will need be running, and which mirrors the same assets byte-for-byte.
|
||||
|
||||
@@ -152,3 +152,14 @@ Example: `example.com,169.254.0.0/16` +
|
||||
Comma separated list of hosts to which all connections should bypass the proxy.
|
||||
Hosts can be specified by name, IP address, or IP range using https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation[CIDR notation].
|
||||
====
|
||||
|
||||
.--http-rewrite
|
||||
[%collapsible]
|
||||
====
|
||||
Default: (none) +
|
||||
Example: `\https://pkg.pkl-lang.org/=https://my.internal.mirror/` +
|
||||
Replace outbound HTTP(S) requests from one URL with another URL.
|
||||
The left-hand side describes the source prefix, and the right-hand describes the target prefix.
|
||||
This option is commonly used to enable package mirroring.
|
||||
The above example will rewrite URL `\https://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.0` to `\https://my.internal.mirror/pkl-k8s/k8s@1.0.0`.
|
||||
====
|
||||
|
||||
@@ -107,3 +107,14 @@ Example: `noProxy = ["example.com", "169.254.0.0/16"]` +
|
||||
Hosts to which all connections should bypass the proxy.
|
||||
Hosts can be specified by name, IP address, or IP range using https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation[CIDR notation].
|
||||
====
|
||||
|
||||
.httpRewrites: MapProperty<String, String>
|
||||
[%collapsible]
|
||||
====
|
||||
Default: `null` +
|
||||
Example: `httpRewrites = [uri("https://pkg.pkl-lang.org/"): uri("https://my.internal.mirror/")]` +
|
||||
Replace outbound HTTP(S) requests from one URL with another URL.
|
||||
The left-hand side describes the source prefix, and the right-hand describes the target prefix.
|
||||
This option is commonly used to enable package mirroring.
|
||||
The above example will rewrite URL `\https://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.0` to `\https://my.internal.mirror/pkl-k8s/k8s@1.0.0`.
|
||||
====
|
||||
|
||||
@@ -119,6 +119,36 @@ class CliMainTest {
|
||||
assertThat(ex.message).contains("URI `file:my file.txt` has invalid syntax")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid rewrites -- non-HTTP URI`() {
|
||||
val ex =
|
||||
assertThrows<BadParameterValue> {
|
||||
rootCmd.parse(arrayOf("eval", "--http-rewrite", "foo=bar", "mymodule.pkl"))
|
||||
}
|
||||
assertThat(ex.message)
|
||||
.contains("Rewrite rule must start with 'http://' or 'https://', but was 'foo'")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid rewrites -- invalid URI`() {
|
||||
val ex =
|
||||
assertThrows<BadParameterValue> {
|
||||
rootCmd.parse(arrayOf("eval", "--http-rewrite", "https://foo bar=baz", "mymodule.pkl"))
|
||||
}
|
||||
assertThat(ex.message).contains("Rewrite target `https://foo bar` has invalid syntax")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid rewrites -- doesn't end with slash`() {
|
||||
val ex =
|
||||
assertThrows<BadParameterValue> {
|
||||
rootCmd.parse(
|
||||
arrayOf("eval", "--http-rewrite", "http://foo.com=https://bar.com", "mymodule.pkl")
|
||||
)
|
||||
}
|
||||
assertThat(ex.message).contains("Rewrite rule must end with '/', but was 'http://foo.com'")
|
||||
}
|
||||
|
||||
private fun makeInput(tempDir: Path, fileName: String = "test.pkl"): String {
|
||||
val code = "x = 1"
|
||||
return tempDir.resolve(fileName).writeString(code).toString()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2025 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.
|
||||
@@ -140,6 +140,9 @@ data class CliBaseOptions(
|
||||
/** Hostnames, IP addresses, or CIDR blocks to not proxy. */
|
||||
val httpNoProxy: List<String>? = null,
|
||||
|
||||
/** URL prefixes to rewrite. */
|
||||
val httpRewrites: Map<URI, URI>? = null,
|
||||
|
||||
/** External module reader process specs */
|
||||
val externalModuleReaders: Map<String, ExternalReader> = mapOf(),
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.pkl.commons.cli
|
||||
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.regex.Pattern
|
||||
@@ -166,29 +167,36 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
||||
|
||||
protected val useColor: Boolean by lazy { cliOptions.color?.hasColor() ?: false }
|
||||
|
||||
private val proxyAddress by lazy {
|
||||
private val proxyAddress: URI? by lazy {
|
||||
cliOptions.httpProxy
|
||||
?: project?.evaluatorSettings?.http?.proxy?.address
|
||||
?: settings.http?.proxy?.address
|
||||
}
|
||||
|
||||
private val noProxy by lazy {
|
||||
private val noProxy: List<String>? by lazy {
|
||||
cliOptions.httpNoProxy
|
||||
?: project?.evaluatorSettings?.http?.proxy?.noProxy
|
||||
?: settings.http?.proxy?.noProxy
|
||||
}
|
||||
|
||||
private val externalModuleReaders by lazy {
|
||||
private val httpRewrites: Map<URI, URI>? by lazy {
|
||||
cliOptions.httpRewrites
|
||||
?: project?.evaluatorSettings?.http?.rewrites
|
||||
?: settings.http?.rewrites()
|
||||
}
|
||||
|
||||
private val externalModuleReaders: Map<String, PklEvaluatorSettings.ExternalReader> by lazy {
|
||||
(project?.evaluatorSettings?.externalModuleReaders ?: emptyMap()) +
|
||||
cliOptions.externalModuleReaders
|
||||
}
|
||||
|
||||
private val externalResourceReaders by lazy {
|
||||
private val externalResourceReaders: Map<String, PklEvaluatorSettings.ExternalReader> by lazy {
|
||||
(project?.evaluatorSettings?.externalResourceReaders ?: emptyMap()) +
|
||||
cliOptions.externalResourceReaders
|
||||
}
|
||||
|
||||
private val externalProcesses by lazy {
|
||||
private val externalProcesses:
|
||||
Map<PklEvaluatorSettings.ExternalReader, ExternalReaderProcess> by lazy {
|
||||
// Share ExternalReaderProcess instances between configured external resource/module readers
|
||||
// with the same spec. This avoids spawning multiple subprocesses if the same reader implements
|
||||
// both reader types and/or multiple schemes.
|
||||
@@ -232,6 +240,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
||||
if ((proxyAddress ?: noProxy) != null) {
|
||||
setProxy(proxyAddress, noProxy ?: listOf())
|
||||
}
|
||||
httpRewrites?.let(::setRewrites)
|
||||
// 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.
|
||||
|
||||
@@ -235,6 +235,39 @@ class BaseOptions : OptionGroup() {
|
||||
.single()
|
||||
.split(",")
|
||||
|
||||
val httpRewrites: Map<URI, URI> by
|
||||
option(
|
||||
names = arrayOf("--http-rewrite"),
|
||||
metavar = "from=to",
|
||||
help = "URL prefixes that should be rewritten.",
|
||||
)
|
||||
.convert { it ->
|
||||
val uris = it.split("=", limit = 2)
|
||||
require(uris.size == 2) { "Rewrites must be in the form of <from>=<to>" }
|
||||
try {
|
||||
val (fromSpec, toSpec) = uris
|
||||
val fromUri = URI(fromSpec).also { IoUtils.validateRewriteRule(it) }
|
||||
val toUri = URI(toSpec).also { IoUtils.validateRewriteRule(it) }
|
||||
fromUri to toUri
|
||||
} catch (e: IllegalArgumentException) {
|
||||
fail(e.message!!)
|
||||
} catch (e: URISyntaxException) {
|
||||
val message = buildString {
|
||||
append("Rewrite target `${e.input}` has invalid syntax (${e.reason}).")
|
||||
if (e.index > -1) {
|
||||
append("\n\n")
|
||||
append(e.input)
|
||||
append("\n")
|
||||
append(" ".repeat(e.index))
|
||||
append("^")
|
||||
}
|
||||
}
|
||||
fail(message)
|
||||
}
|
||||
}
|
||||
.multiple()
|
||||
.toMap()
|
||||
|
||||
val externalModuleReaders: Map<String, ExternalReader> by
|
||||
option(
|
||||
names = arrayOf("--external-module-reader"),
|
||||
@@ -289,6 +322,7 @@ class BaseOptions : OptionGroup() {
|
||||
caCertificates = caCertificates,
|
||||
httpProxy = proxy,
|
||||
httpNoProxy = noProxy ?: emptyList(),
|
||||
httpRewrites = httpRewrites.ifEmpty { null },
|
||||
externalModuleReaders = externalModuleReaders,
|
||||
externalResourceReaders = externalResourceReaders,
|
||||
)
|
||||
|
||||
@@ -517,6 +517,20 @@ public final class EvaluatorBuilder {
|
||||
procs.computeIfAbsent(entry.getValue(), ExternalReaderProcess::of)));
|
||||
}
|
||||
}
|
||||
if (settings.http() != null) {
|
||||
var httpClientBuilder = HttpClient.builder();
|
||||
if (settings.http().proxy() != null) {
|
||||
var noProxy = settings.http().proxy().noProxy();
|
||||
if (noProxy == null) {
|
||||
noProxy = Collections.emptyList();
|
||||
}
|
||||
httpClientBuilder.setProxy(settings.http().proxy().address(), noProxy);
|
||||
}
|
||||
if (settings.http().rewrites() != null) {
|
||||
httpClientBuilder.setRewrites(settings.http().rewrites());
|
||||
}
|
||||
setHttpClient(httpClientBuilder.buildLazily());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.pkl.core.evaluatorSettings;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
@@ -121,15 +123,31 @@ public record PklEvaluatorSettings(
|
||||
externalResourceReaders);
|
||||
}
|
||||
|
||||
public record Http(@Nullable Proxy proxy) {
|
||||
public static final Http DEFAULT = new Http(null);
|
||||
public record Http(@Nullable Proxy proxy, @Nullable Map<URI, URI> rewrites) {
|
||||
public static final Http DEFAULT = new Http(null, Collections.emptyMap());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static @Nullable Http parse(@Nullable Value input) {
|
||||
if (input == null || input instanceof PNull) {
|
||||
return null;
|
||||
} else if (input instanceof PObject http) {
|
||||
var proxy = Proxy.parse((Value) http.getProperty("proxy"));
|
||||
return proxy == null ? DEFAULT : new Http(proxy);
|
||||
var rewrites = http.getProperty("rewrites");
|
||||
if (rewrites instanceof PNull) {
|
||||
return new Http(proxy, null);
|
||||
} else {
|
||||
var parsedRewrites = new HashMap<URI, URI>();
|
||||
for (var entry : ((Map<String, String>) rewrites).entrySet()) {
|
||||
var key = entry.getKey();
|
||||
var value = entry.getValue();
|
||||
try {
|
||||
parsedRewrites.put(new URI(key), new URI(value));
|
||||
} catch (URISyntaxException e) {
|
||||
throw new PklException(ErrorMessages.create("invalidUri", e.getInput()));
|
||||
}
|
||||
}
|
||||
return new Http(proxy, parsedRewrites);
|
||||
}
|
||||
} else {
|
||||
throw PklBugException.unreachableCode();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2025 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.
|
||||
@@ -22,6 +22,7 @@ import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpTimeoutException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
@@ -118,6 +119,10 @@ public interface HttpClient extends AutoCloseable {
|
||||
*/
|
||||
Builder setProxy(@Nullable URI proxyAddress, List<String> noProxy);
|
||||
|
||||
Builder setRewrites(Map<URI, URI> rewrites);
|
||||
|
||||
Builder addRewrite(URI sourcePrefix, URI targetPrefix);
|
||||
|
||||
/**
|
||||
* Creates a new {@code HttpClient} from the current state of this builder.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2025 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.
|
||||
@@ -15,13 +15,17 @@
|
||||
*/
|
||||
package org.pkl.core.http;
|
||||
|
||||
import static org.pkl.core.util.IoUtils.validateRewriteRule;
|
||||
|
||||
import java.net.ProxySelector;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import org.pkl.core.Release;
|
||||
import org.pkl.core.http.HttpClient.Builder;
|
||||
@@ -34,6 +38,7 @@ final class HttpClientBuilder implements HttpClient.Builder {
|
||||
private final List<ByteBuffer> certificateBytes = new ArrayList<>();
|
||||
private int testPort = -1;
|
||||
private ProxySelector proxySelector;
|
||||
private Map<URI, URI> rewrites = new HashMap<>();
|
||||
|
||||
HttpClientBuilder() {
|
||||
var release = Release.current();
|
||||
@@ -87,6 +92,24 @@ final class HttpClientBuilder implements HttpClient.Builder {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setRewrites(Map<URI, URI> rewrites) {
|
||||
for (var entry : rewrites.entrySet()) {
|
||||
validateRewriteRule(entry.getKey());
|
||||
validateRewriteRule(entry.getValue());
|
||||
}
|
||||
this.rewrites = new HashMap<>(rewrites);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder addRewrite(URI sourcePrefix, URI targetPrefix) {
|
||||
validateRewriteRule(sourcePrefix);
|
||||
validateRewriteRule(targetPrefix);
|
||||
this.rewrites.put(sourcePrefix, targetPrefix);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient build() {
|
||||
return doBuild().get();
|
||||
@@ -105,7 +128,7 @@ final class HttpClientBuilder implements HttpClient.Builder {
|
||||
return () -> {
|
||||
var jdkClient =
|
||||
new JdkHttpClient(certificateFiles, certificateBytes, connectTimeout, proxySelector);
|
||||
return new RequestRewritingClient(userAgent, requestTimeout, testPort, jdkClient);
|
||||
return new RequestRewritingClient(userAgent, requestTimeout, testPort, jdkClient, rewrites);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2025 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.
|
||||
@@ -17,13 +17,21 @@ package org.pkl.core.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandler;
|
||||
import java.time.Duration;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import org.pkl.core.PklBugException;
|
||||
import org.pkl.core.util.HttpUtils;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
/**
|
||||
* An {@code HttpClient} decorator that
|
||||
@@ -32,6 +40,7 @@ import org.pkl.core.util.HttpUtils;
|
||||
* <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.
|
||||
* <li>rewrites outbound URI prefixes with another prefix.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Both {@code User-Agent} header and default request timeout are configurable through {@link
|
||||
@@ -44,22 +53,43 @@ final class RequestRewritingClient implements HttpClient {
|
||||
final Duration requestTimeout;
|
||||
final int testPort;
|
||||
final HttpClient delegate;
|
||||
private final List<Entry<URI, URI>> rewrites;
|
||||
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
RequestRewritingClient(
|
||||
String userAgent, Duration requestTimeout, int testPort, HttpClient delegate) {
|
||||
String userAgent,
|
||||
Duration requestTimeout,
|
||||
int testPort,
|
||||
HttpClient delegate,
|
||||
Map<URI, URI> rewrites) {
|
||||
this.userAgent = userAgent;
|
||||
this.requestTimeout = requestTimeout;
|
||||
this.testPort = testPort;
|
||||
this.delegate = delegate;
|
||||
this.rewrites =
|
||||
rewrites.entrySet().stream()
|
||||
.map((it) -> Map.entry(normalizeRewrite(it.getKey()), normalizeRewrite(it.getValue())))
|
||||
.sorted(Comparator.comparingInt((it) -> -it.getKey().toString().length()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||
throws IOException {
|
||||
checkNotClosed(request);
|
||||
return delegate.send(rewriteRequest(request), responseBodyHandler);
|
||||
try {
|
||||
return delegate.send(rewriteRequest(request), responseBodyHandler);
|
||||
} catch (IOException e) {
|
||||
var rewrittenUri = rewriteUri(request.uri());
|
||||
if (rewrittenUri != request.uri()) {
|
||||
throw new IOException(
|
||||
e.getMessage()
|
||||
+ " (request was rewritten: %s -> %s)".formatted(request.uri(), rewrittenUri),
|
||||
e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,11 +129,91 @@ final class RequestRewritingClient implements HttpClient {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private URI rewriteUri(URI uri) {
|
||||
if (testPort != -1 && uri.getPort() == 0) {
|
||||
return HttpUtils.setPort(uri, testPort);
|
||||
private static boolean notEqualCaseInsensitive(@Nullable String a, @Nullable String b) {
|
||||
if (a == null || b == null) {
|
||||
return !Objects.equals(a, b);
|
||||
}
|
||||
return uri;
|
||||
return !a.equalsIgnoreCase(b);
|
||||
}
|
||||
|
||||
// Our docs say to not include query string or fragment in a rewrite rule, but technically they
|
||||
// are supported.
|
||||
public static boolean matchesRewriteRule(URI uri, URI rule) {
|
||||
if (notEqualCaseInsensitive(uri.getScheme(), rule.getScheme())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Objects.equals(uri.getUserInfo(), rule.getUserInfo())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (notEqualCaseInsensitive(uri.getHost(), rule.getHost())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Objects.equals(uri.getPath(), rule.getPath())) {
|
||||
if (uri.getPath() != null
|
||||
&& rule.getPath() != null
|
||||
&& rule.getQuery() == null
|
||||
&& rule.getFragment() == null) {
|
||||
return uri.getPath().startsWith(rule.getPath());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Objects.equals(uri.getQuery(), rule.getQuery())) {
|
||||
if (uri.getQuery() != null && rule.getQuery() != null && rule.getFragment() == null) {
|
||||
return uri.getQuery().startsWith(rule.getQuery());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.getFragment() != null && rule.getFragment() != null) {
|
||||
return uri.getFragment().startsWith(rule.getFragment());
|
||||
}
|
||||
|
||||
return Objects.equals(uri.getFragment(), rule.getFragment());
|
||||
}
|
||||
|
||||
private @Nullable Entry<URI, URI> findRewrite(URI uri) {
|
||||
for (var entry : rewrites) {
|
||||
if (matchesRewriteRule(uri, entry.getKey())) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private URI normalizeRewrite(URI uri) {
|
||||
try {
|
||||
return new URI(
|
||||
uri.getScheme().toLowerCase(),
|
||||
uri.getUserInfo(),
|
||||
uri.getHost().toLowerCase(),
|
||||
uri.getPort(),
|
||||
uri.getPath(),
|
||||
uri.getQuery(),
|
||||
uri.getFragment());
|
||||
} catch (URISyntaxException e) {
|
||||
// impossible condition, we started from a valid URI to begin with
|
||||
throw PklBugException.unreachableCode();
|
||||
}
|
||||
}
|
||||
|
||||
private URI rewriteUri(URI uri) {
|
||||
var rewrite = findRewrite(uri);
|
||||
var ret = uri;
|
||||
if (rewrite != null) {
|
||||
var normalized = normalizeRewrite(uri);
|
||||
var fromUri = rewrite.getKey();
|
||||
var toUri = rewrite.getValue();
|
||||
var relativePath = fromUri.relativize(normalized);
|
||||
ret = toUri.resolve(relativePath);
|
||||
}
|
||||
if (testPort != -1 && ret.getPort() == 0) {
|
||||
ret = HttpUtils.setPort(ret, testPort);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void checkNotClosed(HttpRequest request) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2025 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.
|
||||
@@ -56,6 +56,13 @@ public abstract class AbstractMessagePackEncoder implements MessageEncoder {
|
||||
packer.packMapHeader(size + (value1 != null ? 1 : 0) + (value2 != null ? 1 : 0));
|
||||
}
|
||||
|
||||
protected void packMapHeader(
|
||||
int size, @Nullable Object value1, @Nullable Object value2, @Nullable Object value3)
|
||||
throws IOException {
|
||||
packer.packMapHeader(
|
||||
size + (value1 != null ? 1 : 0) + (value2 != null ? 1 : 0) + (value3 != null ? 1 : 0));
|
||||
}
|
||||
|
||||
protected void packMapHeader(
|
||||
int size,
|
||||
@Nullable Object value1,
|
||||
|
||||
@@ -841,4 +841,17 @@ public final class IoUtils {
|
||||
throw new URISyntaxException(uri.toString(), ErrorMessages.create("invalidOpaqueFileUri"));
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateRewriteRule(URI rewrite) {
|
||||
if (!Objects.equals(rewrite.getScheme(), "http")
|
||||
&& !Objects.equals(rewrite.getScheme(), "https")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Rewrite rule must start with 'http://' or 'https://', but was '%s'".formatted(rewrite));
|
||||
}
|
||||
|
||||
if (!rewrite.toString().endsWith("/")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Rewrite rule must end with '/', but was '%s'".formatted(rewrite));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import "pkl:analyze"
|
||||
|
||||
result = analyze.importGraph(Set("http://localhost:0/foo.pkl"))
|
||||
@@ -1,14 +0,0 @@
|
||||
–– Pkl Error ––
|
||||
HTTP/1.1 header parser received no bytes
|
||||
|
||||
x | result = analyze.importGraph(Set("http://localhost:0/foo.pkl"))
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at analyzeInvalidHttpModule#result (file:///$snippetsDir/input/errors/analyzeInvalidHttpModule.pkl)
|
||||
|
||||
xxx | text = renderer.renderDocument(value)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at pkl.base#Module.output.text (pkl:base)
|
||||
|
||||
xxx | bytes = text.encodeToBytes("UTF-8")
|
||||
^^^^
|
||||
at pkl.base#Module.output.bytes (pkl:base)
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2025 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.
|
||||
@@ -27,7 +27,14 @@ import org.junit.jupiter.api.Test
|
||||
|
||||
class RequestRewritingClientTest {
|
||||
private val captured = RequestCapturingClient()
|
||||
private val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), -1, captured)
|
||||
private val client =
|
||||
RequestRewritingClient(
|
||||
"Pkl",
|
||||
Duration.ofSeconds(42),
|
||||
-1,
|
||||
captured,
|
||||
mapOf(URI("https://foo/") to URI("https://bar/")),
|
||||
)
|
||||
private val exampleUri = URI("https://example.com/foo/bar.html")
|
||||
private val exampleRequest = HttpRequest.newBuilder(exampleUri).build()
|
||||
|
||||
@@ -114,7 +121,7 @@ class RequestRewritingClientTest {
|
||||
@Test
|
||||
fun `rewrites port 0 if test port is set`() {
|
||||
val captured = RequestCapturingClient()
|
||||
val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), 5000, captured)
|
||||
val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), 5000, captured, mapOf())
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com:0")).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
@@ -130,4 +137,175 @@ class RequestRewritingClientTest {
|
||||
|
||||
assertThat(captured.request.uri().port).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `matches rewrite rule`() {
|
||||
fun assertThatRewriteMatches(uri: String, rule: String) =
|
||||
assertThat(RequestRewritingClient.matchesRewriteRule(URI(uri), URI(rule)))
|
||||
.`as`("$uri matches $rule")
|
||||
|
||||
assertThatRewriteMatches("https://www.foo.com/path/to/qux.html", "https://www.foo.com/").isTrue
|
||||
assertThatRewriteMatches("HTTPS://www.foo.com/path/to/qux.html", "https://www.foo.com/").isTrue
|
||||
assertThatRewriteMatches("HTTPS://WWW.FOO.COM/path/to/qux.html", "https://www.foo.com/").isTrue
|
||||
|
||||
assertThatRewriteMatches("https://www.foo.com/path/to/qux.html", "https://www.foo.com/path/")
|
||||
.isTrue
|
||||
assertThatRewriteMatches("https://www.foo.com/path/to/qux.html", "https://www.foo.com/PATH/")
|
||||
.isFalse
|
||||
assertThatRewriteMatches("https://www.foo.com", "https://www.foo.com/").isFalse
|
||||
|
||||
assertThatRewriteMatches(
|
||||
"https://www.foo.com/path/to/qux.html?foo&bar",
|
||||
"https://www.foo.com/path/to/qux.html?foo&bar",
|
||||
)
|
||||
.isTrue
|
||||
assertThatRewriteMatches(
|
||||
"https://www.foo.com/path/to/qux.html?foo&baz",
|
||||
"https://www.foo.com/path/to/qux.html?foo&bar",
|
||||
)
|
||||
.isFalse
|
||||
|
||||
assertThatRewriteMatches(
|
||||
"https://www.foo.com/path/to/qux.html?foo&bar#qux",
|
||||
"https://www.foo.com/path/to/qux.html?foo&bar#q",
|
||||
)
|
||||
.isTrue
|
||||
assertThatRewriteMatches(
|
||||
"https://www.foo.com/path/to/qux.html?foo&bar#qux",
|
||||
"https://www.foo.com/path/to/qux.html?foo&bar#w",
|
||||
)
|
||||
.isFalse
|
||||
assertThatRewriteMatches(
|
||||
"https://www.foo.com/path/to/qux.html?foo&bar",
|
||||
"https://www.foo.com/path/to/qux.html?foo&bar#w",
|
||||
)
|
||||
.isFalse
|
||||
|
||||
assertThatRewriteMatches("https:///", "https:///").isTrue
|
||||
assertThatRewriteMatches("https:///", "http:///").isFalse
|
||||
|
||||
// userinfo
|
||||
assertThatRewriteMatches("https://foo@foo.com/", "http://foo.com/").isFalse
|
||||
assertThatRewriteMatches("https://foo@foo.com/", "http://foo@foo.com/").isFalse
|
||||
assertThatRewriteMatches("https://foo@foo.com/", "http://FOO@foo.com/").isFalse
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rewrites URIs`() {
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://foo.com/bar/baz",
|
||||
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/bar/baz")
|
||||
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://FOO.COM/bar/baz",
|
||||
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/bar/baz")
|
||||
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://foo.com/bar/baz",
|
||||
mapOf(URI("https://FOO.COM/") to URI("https://bar.com/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/bar/baz")
|
||||
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://foo.com/bar/baz",
|
||||
mapOf(URI("https://foo.com/") to URI("https://bar.com/qux/baz/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/qux/baz/bar/baz")
|
||||
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://foo.com/bar/baz",
|
||||
mapOf(URI("https://foo.com/") to URI("https://bar.com/qux/baz/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/qux/baz/bar/baz")
|
||||
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://foo.com/bar/baz?qux=foo#corge",
|
||||
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/bar/baz?qux=foo#corge")
|
||||
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://fooey@foo.com/bar/baz",
|
||||
mapOf(URI("https://fooey@foo.com/") to URI("https://bar.com/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/bar/baz")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rewrites URIs - longest rewrite wins`() {
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://foo.com/qux/bar/baz",
|
||||
mapOf(
|
||||
URI("https://foo.com/") to URI("https://bar.com/"),
|
||||
URI("https://foo.com/qux") to URI("https://corge.com/"),
|
||||
),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://corge.com/bar/baz")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rewrites URIs - hostname is always lowercased`() {
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://foo.com/bar/baz",
|
||||
mapOf(URI("https://FOO.com/") to URI("https://bar.com/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/bar/baz")
|
||||
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://FOO.com/bar/baz",
|
||||
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/bar/baz")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rewrites URIs - scheme is always lowercased`() {
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"HTTPS://foo.com/bar/baz",
|
||||
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/bar/baz")
|
||||
|
||||
assertThat(
|
||||
rewrittenRequest(
|
||||
"https://FOO.com/bar/baz",
|
||||
mapOf(URI("HTTPS://foo.com/") to URI("HTTPS://bar.com/")),
|
||||
)
|
||||
)
|
||||
.isEqualTo("https://bar.com/bar/baz")
|
||||
}
|
||||
|
||||
private fun rewrittenRequest(uri: String, rules: Map<URI, URI>): String {
|
||||
val captured = RequestCapturingClient()
|
||||
val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), -1, captured, rules)
|
||||
val request = HttpRequest.newBuilder(URI(uri)).build()
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
return captured.request.uri().toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ class PklSettingsTest {
|
||||
"pkg.pkl-lang.org"
|
||||
}
|
||||
}
|
||||
rewrites {
|
||||
["https://foo.com/"] = "https://bar.com/"
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
@@ -72,7 +75,8 @@ class PklSettingsTest {
|
||||
PklEvaluatorSettings.Proxy(
|
||||
URI("http://localhost:8080"),
|
||||
listOf("example.com", "pkg.pkl-lang.org"),
|
||||
)
|
||||
),
|
||||
mapOf(URI("https://foo.com/") to URI("https://bar.com/")),
|
||||
)
|
||||
assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp))
|
||||
}
|
||||
@@ -95,7 +99,10 @@ class PklSettingsTest {
|
||||
|
||||
val settings = PklSettings.loadFromPklHomeDir(tempDir)
|
||||
val expectedHttp =
|
||||
PklEvaluatorSettings.Http(PklEvaluatorSettings.Proxy(URI("http://localhost:8080"), listOf()))
|
||||
PklEvaluatorSettings.Http(
|
||||
PklEvaluatorSettings.Proxy(URI("http://localhost:8080"), listOf()),
|
||||
null,
|
||||
)
|
||||
assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp))
|
||||
}
|
||||
|
||||
|
||||
@@ -473,6 +473,7 @@ public class PklPlugin implements Plugin<Project> {
|
||||
task.getTestPort().set(spec.getTestPort());
|
||||
task.getHttpProxy().set(spec.getHttpProxy());
|
||||
task.getHttpNoProxy().set(spec.getHttpNoProxy());
|
||||
task.getHttpRewrites().set(spec.getHttpRewrites());
|
||||
}
|
||||
|
||||
private List<File> getTransitiveModules(AnalyzeImportsTask analyzeTask) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2025 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.
|
||||
@@ -57,4 +57,6 @@ public interface BasePklSpec {
|
||||
Property<URI> getHttpProxy();
|
||||
|
||||
ListProperty<String> getHttpNoProxy();
|
||||
|
||||
MapProperty<URI, URI> getHttpRewrites();
|
||||
}
|
||||
|
||||
@@ -142,6 +142,10 @@ public abstract class BasePklTask extends DefaultTask {
|
||||
@Optional
|
||||
public abstract ListProperty<String> getHttpNoProxy();
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
public abstract MapProperty<URI, URI> getHttpRewrites();
|
||||
|
||||
/**
|
||||
* There are issues with using native libraries in Gradle plugins. As a workaround for now, make
|
||||
* Truffle use an un-optimized runtime.
|
||||
@@ -195,6 +199,7 @@ public abstract class BasePklTask extends DefaultTask {
|
||||
Collections.emptyList(),
|
||||
getHttpProxy().getOrNull(),
|
||||
getHttpNoProxy().getOrElse(List.of()),
|
||||
getHttpRewrites().getOrNull(),
|
||||
Map.of(),
|
||||
Map.of());
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ public abstract class ModulesTask extends BasePklTask {
|
||||
Collections.emptyList(),
|
||||
null,
|
||||
List.of(),
|
||||
getHttpRewrites().getOrNull(),
|
||||
Map.of(),
|
||||
Map.of());
|
||||
}
|
||||
|
||||
@@ -95,7 +95,13 @@ class ServerMessagePackDecoder(unpacker: MessageUnpacker) : BaseMessagePackDecod
|
||||
val httpMap = getNullable(this, "http")?.asMapValue()?.map() ?: return null
|
||||
val proxy = httpMap.unpackProxy()
|
||||
val caCertificates = getNullable(httpMap, "caCertificates")?.asBinaryValue()?.asByteArray()
|
||||
return Http(caCertificates, proxy)
|
||||
val rewrites =
|
||||
getNullable(httpMap, "rewrites")
|
||||
?.asMapValue()
|
||||
?.map()
|
||||
?.mapKeys { it.key.asStringValue().asString() }
|
||||
?.mapValues { it.value.asStringValue().asString() }
|
||||
return Http(caCertificates, proxy, rewrites)
|
||||
}
|
||||
|
||||
private fun Map<Value, Value>.unpackProxy(): Proxy? {
|
||||
|
||||
@@ -36,7 +36,7 @@ class ServerMessagePackEncoder(packer: MessagePacker) : BaseMessagePackEncoder(p
|
||||
}
|
||||
|
||||
private fun MessagePacker.packHttp(http: Http) {
|
||||
packMapHeader(0, http.caCertificates, http.proxy)
|
||||
packMapHeader(0, http.caCertificates, http.proxy, http.rewrites)
|
||||
http.caCertificates?.let { packKeyValue("caCertificates", it) }
|
||||
http.proxy?.let { proxy ->
|
||||
packString("proxy")
|
||||
@@ -44,6 +44,14 @@ class ServerMessagePackEncoder(packer: MessagePacker) : BaseMessagePackEncoder(p
|
||||
packKeyValue("address", proxy.address?.toString())
|
||||
packKeyValue("noProxy", proxy.noProxy)
|
||||
}
|
||||
http.rewrites?.let { rewrites ->
|
||||
packString("rewrites")
|
||||
packMapHeader(rewrites.size)
|
||||
for ((key, value) in rewrites) {
|
||||
packString(key)
|
||||
packString(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessagePacker.packDependencies(dependencies: Map<String, Dependency>) {
|
||||
|
||||
@@ -56,6 +56,8 @@ data class Http(
|
||||
val caCertificates: ByteArray?,
|
||||
/** Proxy settings */
|
||||
val proxy: Proxy?,
|
||||
/** HTTP rewrites */
|
||||
val rewrites: Map<String, String>?,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
@@ -65,12 +67,13 @@ data class Http(
|
||||
if (other.caCertificates == null) return false
|
||||
if (!caCertificates.contentEquals(other.caCertificates)) return false
|
||||
} else if (other.caCertificates != null) return false
|
||||
return Objects.equals(proxy, other.proxy)
|
||||
return Objects.equals(rewrites, other.rewrites) && Objects.equals(proxy, other.proxy)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = caCertificates?.contentHashCode() ?: 0
|
||||
result = 31 * result + (proxy?.hashCode() ?: 0)
|
||||
result = 31 * result + (rewrites?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ class ServerMessagePackCodecTest {
|
||||
Http(
|
||||
proxy = Proxy(URI("http://foo.com:1234"), listOf("bar", "baz")),
|
||||
caCertificates = byteArrayOf(1, 2, 3, 4),
|
||||
rewrites = mapOf("https://foo.com" to "https://bar.com"),
|
||||
),
|
||||
externalModuleReaders = mapOf("external" to externalReader, "external2" to externalReader),
|
||||
externalResourceReaders = mapOf("external" to externalReader),
|
||||
|
||||
@@ -109,10 +109,61 @@ externalModuleReaders: Mapping<String, ExternalReader>?
|
||||
@Since { version = "0.27.0" }
|
||||
externalResourceReaders: Mapping<String, ExternalReader>?
|
||||
|
||||
const local hostnameRegex = Regex(#"https?://([^/?#]*)"#)
|
||||
|
||||
const local hasNonEmptyHostname = (it: String) ->
|
||||
let (hostname = hostnameRegex.findMatchesIn(it).getOrNull(0)?.groups?.getOrNull(1)?.value)
|
||||
hostname != null && hostname.length > 0
|
||||
|
||||
/// A key or value in [Http.rewrites].
|
||||
@Since { version = "0.29.0" }
|
||||
typealias HttpRewrite = String(
|
||||
startsWith(Regex("https?://")),
|
||||
endsWith("/"),
|
||||
hasNonEmptyHostname
|
||||
)
|
||||
|
||||
/// Settings that control how Pkl talks to HTTP(S) servers.
|
||||
class Http {
|
||||
/// Configuration of the HTTP proxy to use.
|
||||
proxy: Proxy?
|
||||
|
||||
/// Replace outbound requests from one URL with another URL.
|
||||
///
|
||||
/// Each key describes the prefix of a request, and each value describes the replacement prefix.
|
||||
///
|
||||
/// This can be useful for setting up mirroring of packages, which are fetched over HTTPS.
|
||||
//
|
||||
/// In the case of multiple matches, the longest prefix is is used.
|
||||
///
|
||||
/// The URL hostname is case-insensitive.
|
||||
///
|
||||
/// A replacement only happens once when resolving URLs.
|
||||
/// Therefore, the first replacement is the final URL that is used for the outbound request.
|
||||
///
|
||||
/// In the following example, an original request for `https://pkg.pkl-lang.org/my/pkg@1.0.0` is
|
||||
/// replaced with `https://my.internal.mirror/my/pkg@1.0.0`.
|
||||
///
|
||||
/// This does not affect `3XX` status code redirect following.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```
|
||||
/// rewrites {
|
||||
/// ["https://pkg.pkl-lang.org/"] = "https://my.internal.mirror/"
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Rewrite targets must satisfy the following:
|
||||
///
|
||||
/// * It starts with either `http://`, or `https://`.
|
||||
/// * It ends with `/`.
|
||||
/// * It has a non-empty hostname.
|
||||
///
|
||||
/// An rewrite target should also not contain a query string or fragment component
|
||||
/// (not schematically enforced).
|
||||
@Since { version = "0.29.0" }
|
||||
rewrites: Mapping<HttpRewrite, HttpRewrite>?
|
||||
}
|
||||
|
||||
/// Settings that control how Pkl talks to HTTP proxies.
|
||||
|
||||
Reference in New Issue
Block a user