Add support for HTTP proxying (#506)

* Add `--proxy` and `--no-proxy` CLI flags
* Add property `http` to `pkl:settings`
* Move `EvaluatorSettings` from `pkl:Project` to its own module and add property `http`
* Add support for proxying in server mode, and through Gradle
* Add `setProxy()` to `HttpClient`
* Add documentation
This commit is contained in:
Philip K.F. Hölzenspies
2024-06-12 19:54:22 +01:00
committed by GitHub
parent a520ae7d04
commit b03530ed1f
61 changed files with 1581 additions and 412 deletions

View File

@@ -440,39 +440,39 @@ public final class EvaluatorBuilder {
*/
public EvaluatorBuilder applyFromProject(Project project) {
this.dependencies = project.getDependencies();
var settings = project.getSettings();
var settings = project.getEvaluatorSettings();
if (securityManager != null) {
throw new IllegalStateException(
"Cannot call both `setSecurityManager` and `setProject`, because both define security manager settings. Call `setProjectOnly` if the security manager is desired.");
}
if (settings.getAllowedModules() != null) {
setAllowedModules(settings.getAllowedModules());
if (settings.allowedModules() != null) {
setAllowedModules(settings.allowedModules());
}
if (settings.getAllowedResources() != null) {
setAllowedResources(settings.getAllowedResources());
if (settings.allowedResources() != null) {
setAllowedResources(settings.allowedResources());
}
if (settings.getExternalProperties() != null) {
setExternalProperties(settings.getExternalProperties());
if (settings.externalProperties() != null) {
setExternalProperties(settings.externalProperties());
}
if (settings.getEnv() != null) {
setEnvironmentVariables(settings.getEnv());
if (settings.env() != null) {
setEnvironmentVariables(settings.env());
}
if (settings.getTimeout() != null) {
setTimeout(settings.getTimeout().toJavaDuration());
if (settings.timeout() != null) {
setTimeout(settings.timeout().toJavaDuration());
}
if (settings.getModulePath() != null) {
if (settings.modulePath() != null) {
// indirectly closed by `ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)`
var modulePathResolver = new ModulePathResolver(settings.getModulePath());
var modulePathResolver = new ModulePathResolver(settings.modulePath());
addResourceReader(ResourceReaders.modulePath(modulePathResolver));
addModuleKeyFactory(ModuleKeyFactories.modulePath(modulePathResolver));
}
if (settings.getRootDir() != null) {
setRootDir(settings.getRootDir());
if (settings.rootDir() != null) {
setRootDir(settings.rootDir());
}
if (Boolean.TRUE.equals(settings.isNoCache())) {
if (Boolean.TRUE.equals(settings.noCache())) {
setModuleCacheDir(null);
} else if (settings.getModuleCacheDir() != null) {
setModuleCacheDir(settings.getModuleCacheDir());
} else if (settings.moduleCacheDir() != null) {
setModuleCacheDir(settings.moduleCacheDir());
}
return this;
}

View File

@@ -108,7 +108,7 @@ public final class StackFrameTransformers {
public static StackFrameTransformer createDefault(PklSettings settings) {
return defaultTransformer
// order is relevant
.andThen(convertFilePathToUriScheme(settings.getEditor().getUrlScheme()));
.andThen(convertFilePathToUriScheme(settings.editor().urlScheme()));
}
private static StackFrameTransformer loadFromServiceProviders() {

View File

@@ -0,0 +1,191 @@
/**
* 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.evaluatorSettings;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import org.pkl.core.Duration;
import org.pkl.core.PNull;
import org.pkl.core.PObject;
import org.pkl.core.PklBugException;
import org.pkl.core.PklException;
import org.pkl.core.Value;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.Nullable;
/** Java version of {@code pkl.EvaluatorSettings}. */
public record PklEvaluatorSettings(
@Nullable Map<String, String> externalProperties,
@Nullable Map<String, String> env,
@Nullable List<Pattern> allowedModules,
@Nullable List<Pattern> allowedResources,
@Nullable Boolean noCache,
@Nullable Path moduleCacheDir,
@Nullable List<Path> modulePath,
@Nullable Duration timeout,
@Nullable Path rootDir,
@Nullable Http http) {
/** Initializes a {@link PklEvaluatorSettings} from a raw object representation. */
@SuppressWarnings("unchecked")
public static PklEvaluatorSettings parse(
Value input, BiFunction<? super String, ? super String, Path> pathNormalizer) {
if (!(input instanceof PObject pSettings)) {
throw PklBugException.unreachableCode();
}
var moduleCacheDirStr = (String) pSettings.get("moduleCacheDir");
var moduleCacheDir =
moduleCacheDirStr == null
? null
: pathNormalizer.apply(moduleCacheDirStr, "moduleCacheDir");
var allowedModulesStrs = (List<String>) pSettings.get("allowedModules");
var allowedModules =
allowedModulesStrs == null
? null
: allowedModulesStrs.stream().map(Pattern::compile).toList();
var allowedResourcesStrs = (List<String>) pSettings.get("allowedResources");
var allowedResources =
allowedResourcesStrs == null
? null
: allowedResourcesStrs.stream().map(Pattern::compile).toList();
var modulePathStrs = (List<String>) pSettings.get("modulePath");
var modulePath =
modulePathStrs == null
? null
: modulePathStrs.stream().map(it -> pathNormalizer.apply(it, "modulePath")).toList();
var rootDirStr = (String) pSettings.get("rootDir");
var rootDir = rootDirStr == null ? null : pathNormalizer.apply(rootDirStr, "rootDir");
return new PklEvaluatorSettings(
(Map<String, String>) pSettings.get("externalProperties"),
(Map<String, String>) pSettings.get("env"),
allowedModules,
allowedResources,
(Boolean) pSettings.get("noCache"),
moduleCacheDir,
modulePath,
(Duration) pSettings.get("timeout"),
rootDir,
Http.parse((Value) pSettings.get("http")));
}
public record Http(@Nullable Proxy proxy) {
public static final Http DEFAULT = new Http(null);
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);
} else {
throw PklBugException.unreachableCode();
}
}
}
public record Proxy(@Nullable URI address, @Nullable List<String> noProxy) {
public static Proxy create(@Nullable String address, @Nullable List<String> noProxy) {
URI addressUri;
try {
addressUri = address == null ? null : new URI(address);
} catch (URISyntaxException e) {
throw new PklException(ErrorMessages.create("invalidUri", address));
}
return new Proxy(addressUri, noProxy);
}
@SuppressWarnings("unchecked")
public static @Nullable Proxy parse(Value input) {
if (input instanceof PNull) {
return null;
} else if (input instanceof PObject proxy) {
var address = (String) proxy.get("address");
var noProxy = (List<String>) proxy.get("noProxy");
return create(address, noProxy);
} else {
throw PklBugException.unreachableCode();
}
}
}
private boolean arePatternsEqual(
@Nullable List<Pattern> thesePatterns, @Nullable List<Pattern> thosePatterns) {
if (thesePatterns == null) {
return thosePatterns == null;
}
if (thosePatterns == null || thesePatterns.size() != thosePatterns.size()) {
return false;
}
for (var i = 0; i < thesePatterns.size(); i++) {
if (!thesePatterns.get(i).pattern().equals(thosePatterns.get(i).pattern())) {
return false;
}
}
return true;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PklEvaluatorSettings that)) {
return false;
}
return Objects.equals(externalProperties, that.externalProperties)
&& Objects.equals(env, that.env)
&& arePatternsEqual(allowedModules, that.allowedModules)
&& arePatternsEqual(allowedResources, that.allowedResources)
&& Objects.equals(noCache, that.noCache)
&& Objects.equals(moduleCacheDir, that.moduleCacheDir)
&& Objects.equals(timeout, that.timeout)
&& Objects.equals(rootDir, that.rootDir)
&& Objects.equals(http, that.http);
}
private int hashPatterns(@Nullable List<Pattern> patterns) {
if (patterns == null) {
return 0;
}
var ret = 1;
for (var pattern : patterns) {
ret = 31 * ret + pattern.pattern().hashCode();
}
return ret;
}
@Override
public int hashCode() {
var result =
Objects.hash(externalProperties, env, noCache, moduleCacheDir, timeout, rootDir, http);
result = 31 * result + hashPatterns(allowedModules);
result = 31 * result + hashPatterns(allowedResources);
return result;
}
}

View File

@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.core.evaluatorSettings;
import org.pkl.core.util.NonnullByDefault;

View File

@@ -21,7 +21,9 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.nio.file.Path;
import java.util.List;
import javax.net.ssl.SSLContext;
import org.pkl.core.util.Nullable;
/**
* An HTTP client.
@@ -36,6 +38,7 @@ import javax.net.ssl.SSLContext;
public interface HttpClient extends AutoCloseable {
/** A builder of {@linkplain HttpClient HTTP clients}. */
@SuppressWarnings("unused")
interface Builder {
/**
* Sets the {@code User-Agent} header.
@@ -116,6 +119,32 @@ public interface HttpClient extends AutoCloseable {
*/
Builder setTestPort(int port);
/**
* Sets the proxy selector to use when establishing connections.
*
* <p>Defaults to: {@link java.net.ProxySelector#getDefault()}.
*/
Builder setProxySelector(java.net.ProxySelector proxySelector);
/**
* Configures HTTP connections to connect to the provided proxy address.
*
* <p>The provided {@code proxyAddress} must have scheme http, not contain userInfo, and not
* have a path segment.
*
* <p>If {@code proxyAddress} is {@code null}, uses the proxy address provided by {@link
* java.net.ProxySelector#getDefault()}.
*
* <p>NOTE: Due to a <a href="https://bugs.openjdk.org/browse/JDK-8256409">limitation in the
* JDK</a>, this does not configure the proxy server used for certificate revocation checking.
* To configure the certificate revocation checker, the result of {@link
* java.net.ProxySelector#getDefault} needs to be changed either by setting system properties,
* or via {@link java.net.ProxySelector#setDefault}.
*
* @throws IllegalArgumentException if `proxyAddress` is invalid.
*/
Builder setProxy(@Nullable URI proxyAddress, List<String> noProxy);
/**
* Creates a new {@code HttpClient} from the current state of this builder.
*

View File

@@ -16,6 +16,7 @@
package org.pkl.core.http;
import java.io.IOException;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
@@ -25,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import org.pkl.core.Release;
import org.pkl.core.http.HttpClient.Builder;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.IoUtils;
@@ -36,6 +38,7 @@ final class HttpClientBuilder implements HttpClient.Builder {
private final List<Path> certificateFiles = new ArrayList<>();
private final List<URI> certificateUris = new ArrayList<>();
private int testPort = -1;
private ProxySelector proxySelector;
HttpClientBuilder() {
this(IoUtils.getPklHomeDir().resolve("cacerts"));
@@ -109,6 +112,17 @@ final class HttpClientBuilder implements HttpClient.Builder {
return this;
}
public HttpClient.Builder setProxySelector(ProxySelector proxySelector) {
this.proxySelector = proxySelector;
return this;
}
@Override
public Builder setProxy(URI proxyAddress, List<String> noProxy) {
this.proxySelector = new org.pkl.core.http.ProxySelector(proxyAddress, noProxy);
return this;
}
@Override
public HttpClient build() {
return doBuild().get();
@@ -123,8 +137,11 @@ final class HttpClientBuilder implements HttpClient.Builder {
// make defensive copies because Supplier may get called after builder was mutated
var certificateFiles = List.copyOf(this.certificateFiles);
var certificateUris = List.copyOf(this.certificateUris);
var proxySelector =
this.proxySelector != null ? this.proxySelector : java.net.ProxySelector.getDefault();
return () -> {
var jdkClient = new JdkHttpClient(certificateFiles, certificateUris, connectTimeout);
var jdkClient =
new JdkHttpClient(certificateFiles, certificateUris, connectTimeout, proxySelector);
return new RequestRewritingClient(userAgent, requestTimeout, testPort, jdkClient);
};
}

View File

@@ -77,11 +77,16 @@ final class JdkHttpClient implements HttpClient {
closeMethod = result;
}
JdkHttpClient(List<Path> certificateFiles, List<URI> certificateUris, Duration connectTimeout) {
JdkHttpClient(
List<Path> certificateFiles,
List<URI> certificateUris,
Duration connectTimeout,
java.net.ProxySelector proxySelector) {
underlying =
java.net.http.HttpClient.newBuilder()
.sslContext(createSslContext(certificateFiles, certificateUris))
.connectTimeout(connectTimeout)
.proxy(proxySelector)
.followRedirects(Redirect.NORMAL)
.build();
}

View File

@@ -0,0 +1,215 @@
/**
* 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.math.BigInteger;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.regex.Pattern;
import org.pkl.core.util.Nullable;
/**
* Represents a noproxy entry.
*
* <p>Follows the rules described in <a
* href="https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/#standardizing-no_proxy">Standardizing
* {@code no_proxy}</a>
*/
final class NoProxyRule {
private static final String portString = "(?::(?<port>\\d{1,5}))?";
private static final String cidrString = "(?:/(?<cidr>\\d{1,3}))?";
private static final String ipv4AddressString =
"(?<host>[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})";
private static final Pattern ipv4Address = Pattern.compile("^" + ipv4AddressString + "$");
private static final Pattern ipv4AddressOrCidr =
Pattern.compile("^" + ipv4AddressString + cidrString + portString + "$");
private static final String ipv6AddressString =
"(?<host>(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(?:ffff(:0{1,4})?:)?(?:(?:25[0-5]|(2[0-4]|1?[0-9])?[0-9])\\.){3}(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])\\.){3}(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9]))";
private static final Pattern ipv6AddressOrCidr =
Pattern.compile(
"^(?<open>\\[)?" + ipv6AddressString + cidrString + "(?<close>])?" + portString + "$");
private static final Pattern hostnamePattern =
Pattern.compile("^\\.?(?<host>[^:]+)" + portString + "$");
private @Nullable Integer ipv4 = null;
private @Nullable Integer ipv4Mask = null;
private @Nullable BigInteger ipv6 = null;
private @Nullable BigInteger ipv6Mask = null;
private @Nullable String hostname = null;
private int port = 0;
private boolean allNoProxy = false;
public NoProxyRule(String repr) {
if (repr.equals("*")) {
allNoProxy = true;
return;
}
var ipv4Matcher = ipv4AddressOrCidr.matcher(repr);
if (ipv4Matcher.matches()) {
var ipAddress = ipv4Matcher.group("host");
ipv4 = parseIpv4(ipAddress);
if (ipv4Matcher.group("cidr") != null) {
var prefixLength = Integer.parseInt(ipv4Matcher.group("cidr"));
if (prefixLength > 32) {
// best-effort (don't fail on invalid cidrs).
hostname = repr;
}
ipv4Mask = 0xffffffff << (32 - prefixLength);
}
if (ipv4Matcher.group("port") != null) {
port = Integer.parseInt(ipv4Matcher.group("port"));
}
return;
}
var ipv6Matcher = ipv6AddressOrCidr.matcher(repr);
if (ipv6Matcher.matches()) {
var ipAddress = ipv6Matcher.group("host");
ipv6 = parseIpv6(ipAddress);
if (ipv6Matcher.group("cidr") != null) {
var maskBuffer = ByteBuffer.allocate(16).putLong(-1L).putLong(-1L);
var prefixLength = Integer.parseInt(ipv6Matcher.group("cidr"));
if (prefixLength > 128) {
// best-effort (don't fail on invalid cidrs).
hostname = repr;
return;
}
ipv6Mask = new BigInteger(1, maskBuffer.array()).not().shiftRight(prefixLength);
}
if (ipv6Matcher.group("port") != null) {
port = Integer.parseInt(ipv6Matcher.group("port"));
}
return;
}
var hostnameMatcher = hostnamePattern.matcher(repr);
if (hostnameMatcher.matches()) {
hostname = hostnameMatcher.group("host");
if (hostnameMatcher.group("port") != null) {
port = Integer.parseInt(hostnameMatcher.group("port"));
}
return;
}
throw new RuntimeException("Failed to parse hostname in no-proxy rule: " + repr);
}
public boolean matches(URI uri) {
if (allNoProxy) {
return true;
}
if (!hostMatches(uri)) {
return false;
}
if (port == 0) {
return true;
}
var thatPort = uri.getPort();
if (thatPort == -1) {
thatPort =
switch (uri.getScheme()) {
case "http" -> 80;
case "https" -> 443;
default -> -1;
};
}
return port == thatPort;
}
/** Tells if the provided URI should not be proxied according to the rules described. */
public boolean hostMatches(URI uri) {
if (allNoProxy) {
return true;
}
var host = uri.getHost();
if (host == null) {
return false;
}
if (host.equalsIgnoreCase(hostname)) {
return true;
}
if (hostname != null && endsWithIgnoreCase(host, "." + hostname)) {
return true;
}
return ipV6Matches(uri.getHost()) || ipV4Matches(uri.getHost());
}
private boolean endsWithIgnoreCase(String str, String suffix) {
var len = suffix.length();
return str.regionMatches(true, str.length() - len, suffix, 0, len);
}
private boolean ipV4Matches(String hostname) {
if (ipv4 == null) {
return false;
}
if (!ipv4Address.matcher(hostname).matches()) {
return false;
}
var address = parseIpv4(hostname);
if (ipv4.equals(address)) {
return true;
}
if (ipv4Mask != null) {
return (ipv4 & ipv4Mask) == (address & ipv4Mask);
}
return false;
}
private boolean ipV6Matches(String hostname) {
if (ipv6 == null) {
return false;
}
if (!hostname.startsWith("[") && !hostname.endsWith("]")) {
return false;
}
var ipv6Repr = hostname.substring(1, hostname.length() - 1);
// According to RFC3986, square brackets can _only_ surround IPV6 addresses, so it should be
// safe to straight up parse it.
// <https://www.ietf.org/rfc/rfc3986.txt>
var address = parseIpv6(ipv6Repr);
if (ipv6.equals(address)) {
return true;
}
if (ipv6Mask != null) {
return ipv6.and(ipv6Mask).equals(address.and(ipv6Mask));
}
return false;
}
private BigInteger parseIpv6(String repr) {
try {
var inet = Inet6Address.getByName(repr);
var byteArr = inet.getAddress();
return new BigInteger(1, byteArr);
} catch (UnknownHostException e) {
// should never happen; `repr` is an IPV6 literal.
throw new RuntimeException(
"Received unexpected UnknownHostException during parsing IPV6 literal", e);
}
}
private int parseIpv4(String repr) {
try {
var inet = Inet4Address.getByName(repr);
return ByteBuffer.wrap(inet.getAddress()).getInt();
} catch (UnknownHostException e) {
// should never happen; `repr` is an IPV4 literal.
throw new RuntimeException(
"Received unexpected UnknownHostException during parsing IPV4 literal", e);
}
}
}

View File

@@ -0,0 +1,78 @@
/**
* 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 com.oracle.truffle.api.nodes.ExplodeLoop;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
import java.net.URI;
import java.util.List;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.Nullable;
final class ProxySelector extends java.net.ProxySelector {
public static final List<Proxy> NO_PROXY = List.of(Proxy.NO_PROXY);
private final @Nullable List<Proxy> myProxy;
private final List<NoProxyRule> noProxyRules;
private final @Nullable java.net.ProxySelector delegate;
ProxySelector(@Nullable URI proxyAddress, List<String> noProxyRules) {
this.noProxyRules = noProxyRules.stream().map(NoProxyRule::new).toList();
if (proxyAddress == null) {
this.delegate = java.net.ProxySelector.getDefault();
this.myProxy = null;
} else {
if (!proxyAddress.getScheme().equalsIgnoreCase("http")
|| proxyAddress.getHost() == null
|| !proxyAddress.getPath().isEmpty()
|| proxyAddress.getUserInfo() != null) {
throw new IllegalArgumentException(
ErrorMessages.create("malformedProxyAddress", proxyAddress));
}
this.delegate = null;
var port = proxyAddress.getPort();
if (port == -1) {
port = 80;
}
this.myProxy =
List.of(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyAddress.getHost(), port)));
}
}
@Override
@ExplodeLoop
public List<Proxy> select(URI uri) {
for (var proxyRule : noProxyRules) {
if (proxyRule.matches(uri)) {
return NO_PROXY;
}
}
if (delegate != null) {
return delegate.select(uri);
}
assert myProxy != null;
return myProxy;
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
/* ignore */
}
}

View File

@@ -39,7 +39,9 @@ import org.pkl.core.SecurityManager;
import org.pkl.core.SecurityManagers;
import org.pkl.core.StackFrameTransformer;
import org.pkl.core.StackFrameTransformers;
import org.pkl.core.Value;
import org.pkl.core.Version;
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings;
import org.pkl.core.module.ModuleKeyFactories;
import org.pkl.core.packages.Checksums;
import org.pkl.core.packages.Dependency.RemoteDependency;
@@ -54,7 +56,7 @@ import org.pkl.core.util.Nullable;
public final class Project {
private final @Nullable Package pkg;
private final DeclaredDependencies dependencies;
private final EvaluatorSettings evaluatorSettings;
private final PklEvaluatorSettings evaluatorSettings;
private final URI projectFileUri;
private final URI projectBaseUri;
private final List<URI> tests;
@@ -178,7 +180,9 @@ public final class Project {
getProperty(
module,
"evaluatorSettings",
(settings) -> parseEvaluatorSettings(settings, projectBaseUri));
(settings) ->
PklEvaluatorSettings.parse(
(Value) settings, (it, name) -> resolveNullablePath(it, projectBaseUri, name)));
@SuppressWarnings("unchecked")
var testPathStrs = (List<String>) getProperty(module, "tests");
var tests =
@@ -210,51 +214,6 @@ public final class Project {
return result;
}
@SuppressWarnings("unchecked")
private static EvaluatorSettings parseEvaluatorSettings(Object settings, URI projectBaseUri) {
var pSettings = (PObject) settings;
var externalProperties = getNullableProperty(pSettings, "externalProperties", Project::asMap);
var env = getNullableProperty(pSettings, "env", Project::asMap);
var allowedModules = getNullableProperty(pSettings, "allowedModules", Project::asPatternList);
var allowedResources =
getNullableProperty(pSettings, "allowedResources", Project::asPatternList);
var noCache = (Boolean) getNullableProperty(pSettings, "noCache");
var modulePathStrs = (List<String>) getNullableProperty(pSettings, "modulePath");
var timeout = (Duration) getNullableProperty(pSettings, "timeout");
List<Path> modulePath = null;
if (modulePathStrs != null) {
modulePath =
modulePathStrs.stream()
.map((it) -> resolveNullablePath(it, projectBaseUri, "modulePath"))
.collect(Collectors.toList());
}
var moduleCacheDir = getNullablePath(pSettings, "moduleCacheDir", projectBaseUri);
var rootDir = getNullablePath(pSettings, "rootDir", projectBaseUri);
return new EvaluatorSettings(
externalProperties,
env,
allowedModules,
allowedResources,
noCache,
moduleCacheDir,
modulePath,
timeout,
rootDir);
}
@SuppressWarnings("unchecked")
private static Map<String, String> asMap(Object t) {
assert t instanceof Map;
return (Map<String, String>) t;
}
@SuppressWarnings("unchecked")
private static List<Pattern> asPatternList(Object t) {
return ((List<String>) t).stream().map(Pattern::compile).collect(Collectors.toList());
}
private static Object getProperty(PObject settings, String propertyName) {
return settings.getProperty(propertyName);
}
@@ -307,12 +266,6 @@ public final class Project {
}
}
private static @Nullable Path getNullablePath(
Composite object, String propertyName, URI projectBaseUri) {
return resolveNullablePath(
(String) getNullableProperty(object, propertyName), projectBaseUri, propertyName);
}
@SuppressWarnings("unchecked")
private static Package parsePackage(PObject pObj) throws URISyntaxException {
var name = (String) pObj.getProperty("name");
@@ -353,7 +306,7 @@ public final class Project {
private Project(
@Nullable Package pkg,
DeclaredDependencies dependencies,
EvaluatorSettings evaluatorSettings,
PklEvaluatorSettings evaluatorSettings,
URI projectFileUri,
URI projectBaseUri,
List<URI> tests,
@@ -371,7 +324,13 @@ public final class Project {
return pkg;
}
/** Use {@link org.pkl.core.project.Project#getEvaluatorSettings()} instead. */
@Deprecated(forRemoval = true)
public EvaluatorSettings getSettings() {
return new EvaluatorSettings(evaluatorSettings);
}
public PklEvaluatorSettings getEvaluatorSettings() {
return evaluatorSettings;
}
@@ -430,17 +389,13 @@ public final class Project {
return Path.of(projectBaseUri);
}
@Deprecated(forRemoval = true)
public static class EvaluatorSettings {
private final PklEvaluatorSettings delegate;
private final @Nullable Map<String, String> externalProperties;
private final @Nullable Map<String, String> env;
private final @Nullable List<Pattern> allowedModules;
private final @Nullable List<Pattern> allowedResources;
private final @Nullable Boolean noCache;
private final @Nullable Path moduleCacheDir;
private final @Nullable List<Path> modulePath;
private final @Nullable Duration timeout;
private final @Nullable Path rootDir;
public EvaluatorSettings(PklEvaluatorSettings delegate) {
this.delegate = delegate;
}
public EvaluatorSettings(
@Nullable Map<String, String> externalProperties,
@@ -452,81 +407,63 @@ public final class Project {
@Nullable List<Path> modulePath,
@Nullable Duration timeout,
@Nullable Path rootDir) {
this.externalProperties = externalProperties;
this.env = env;
this.allowedModules = allowedModules;
this.allowedResources = allowedResources;
this.noCache = noCache;
this.moduleCacheDir = moduleCacheDir;
this.modulePath = modulePath;
this.timeout = timeout;
this.rootDir = rootDir;
this.delegate =
new PklEvaluatorSettings(
externalProperties,
env,
allowedModules,
allowedResources,
noCache,
moduleCacheDir,
modulePath,
timeout,
rootDir,
null);
}
@Deprecated(forRemoval = true)
public @Nullable Map<String, String> getExternalProperties() {
return externalProperties;
return delegate.externalProperties();
}
@Deprecated(forRemoval = true)
public @Nullable Map<String, String> getEnv() {
return env;
return delegate.env();
}
@Deprecated(forRemoval = true)
public @Nullable List<Pattern> getAllowedModules() {
return allowedModules;
return delegate.allowedModules();
}
@Deprecated(forRemoval = true)
public @Nullable List<Pattern> getAllowedResources() {
return allowedResources;
return delegate.allowedResources();
}
@Deprecated(forRemoval = true)
public @Nullable Boolean isNoCache() {
return noCache;
return delegate.noCache();
}
@Deprecated(forRemoval = true)
public @Nullable List<Path> getModulePath() {
return modulePath;
return delegate.modulePath();
}
@Deprecated(forRemoval = true)
public @Nullable Duration getTimeout() {
return timeout;
return delegate.timeout();
}
@Deprecated(forRemoval = true)
public @Nullable Path getModuleCacheDir() {
return moduleCacheDir;
return delegate.moduleCacheDir();
}
@Deprecated(forRemoval = true)
public @Nullable Path getRootDir() {
return rootDir;
}
private boolean arePatternsEqual(
@Nullable List<Pattern> myPattern, @Nullable List<Pattern> thatPattern) {
if (myPattern == null) {
return thatPattern == null;
}
if (thatPattern == null) {
return false;
}
if (myPattern.size() != thatPattern.size()) {
return false;
}
for (var i = 0; i < myPattern.size(); i++) {
if (!myPattern.get(i).pattern().equals(thatPattern.get(i).pattern())) {
return false;
}
}
return true;
}
private int hashPatterns(@Nullable List<Pattern> patterns) {
if (patterns == null) {
return 0;
}
var ret = 1;
for (var pattern : patterns) {
ret = 31 * ret + pattern.pattern().hashCode();
}
return ret;
return delegate.rootDir();
}
@Override
@@ -534,52 +471,37 @@ public final class Project {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EvaluatorSettings that = (EvaluatorSettings) o;
return Objects.equals(externalProperties, that.externalProperties)
&& Objects.equals(env, that.env)
&& arePatternsEqual(allowedModules, that.allowedModules)
&& arePatternsEqual(allowedResources, that.allowedResources)
&& Objects.equals(noCache, that.noCache)
&& Objects.equals(moduleCacheDir, that.moduleCacheDir)
&& Objects.equals(modulePath, that.modulePath)
&& Objects.equals(timeout, that.timeout)
&& Objects.equals(rootDir, that.rootDir);
return o != null
&& getClass() == o.getClass()
&& Objects.equals(delegate, ((EvaluatorSettings) o).delegate);
}
@Override
public int hashCode() {
var result =
Objects.hash(
externalProperties, env, noCache, moduleCacheDir, modulePath, timeout, rootDir);
result = 31 * result + hashPatterns(allowedModules);
result = 31 * result + hashPatterns(allowedResources);
return result;
return delegate.hashCode();
}
@Override
public String toString() {
return "EvaluatorSettings{"
+ "externalProperties="
+ externalProperties
+ delegate.externalProperties()
+ ", env="
+ env
+ delegate.env()
+ ", allowedModules="
+ allowedModules
+ delegate.allowedModules()
+ ", allowedResources="
+ allowedResources
+ delegate.allowedResources()
+ ", noCache="
+ noCache
+ delegate.noCache()
+ ", moduleCacheDir="
+ moduleCacheDir
+ delegate.moduleCacheDir()
+ ", modulePath="
+ modulePath
+ delegate.modulePath()
+ ", timeout="
+ timeout
+ delegate.timeout()
+ ", rootDir="
+ rootDir
+ delegate.rootDir()
+ '}';
}
}

View File

@@ -20,9 +20,11 @@ import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
import org.pkl.core.*;
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings;
import org.pkl.core.module.ModuleKeyFactories;
import org.pkl.core.resource.ResourceReaders;
import org.pkl.core.runtime.VmEvalException;
import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable;
@@ -32,19 +34,13 @@ import org.pkl.core.util.Nullable;
* {@code load} methods.
*/
// keep in sync with stdlib/settings.pkl
public final class PklSettings {
public record PklSettings(Editor editor, @Nullable PklEvaluatorSettings.Http http) {
private static final List<Pattern> ALLOWED_MODULES =
List.of(Pattern.compile("pkl:"), Pattern.compile("file:"));
private static final List<Pattern> ALLOWED_RESOURCES =
List.of(Pattern.compile("env:"), Pattern.compile("file:"));
private final Editor editor;
public PklSettings(Editor editor) {
this.editor = editor;
}
/**
* Loads the user settings file ({@literal ~/.pkl/settings.pkl}). If this file does not exist,
* returns default settings defined by module {@literal pkl.settings}.
@@ -56,7 +52,9 @@ public final class PklSettings {
/** For testing only. */
static PklSettings loadFromPklHomeDir(Path pklHomeDir) throws VmEvalException {
var path = pklHomeDir.resolve("settings.pkl");
return Files.exists(path) ? load(ModuleSource.path(path)) : new PklSettings(Editor.SYSTEM);
return Files.exists(path)
? load(ModuleSource.path(path))
: new PklSettings(Editor.SYSTEM, null);
}
/** Loads a settings file from the given path. */
@@ -73,46 +71,34 @@ public final class PklSettings {
.addEnvironmentVariables(System.getenv())
.build()) {
var module = evaluator.evaluateOutputValueAs(moduleSource, PClassInfo.Settings);
return parseSettings(module);
return parseSettings(module, moduleSource);
}
}
private static PklSettings parseSettings(PObject module) throws VmEvalException {
// can't use object mapping in pkl-core, so map manually
var editor = (PObject) module.getProperty("editor");
var urlScheme = (String) editor.getProperty("urlScheme");
return new PklSettings(new Editor(urlScheme));
private static PklSettings parseSettings(PObject module, ModuleSource location)
throws VmEvalException {
if (!(module.getPropertyOrNull("editor") instanceof PObject pObject)
|| !(pObject.getPropertyOrNull("urlScheme") instanceof String str)) {
throw new VmExceptionBuilder().evalError("invalidSettingsFile", location.getUri()).build();
}
var editor = new Editor(str);
var httpSettings = PklEvaluatorSettings.Http.parse((Value) module.getProperty("http"));
return new PklSettings(editor, httpSettings);
}
/** Returns the editor for viewing and editing Pkl files. */
/**
* Returns the editor for viewing and editing Pkl files.
*
* <p>This method is deprecated, use {@link #editor()} instead.
*/
@Deprecated(forRemoval = true)
public Editor getEditor() {
return editor;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
var that = (PklSettings) o;
return editor.equals(that.editor);
}
@Override
public int hashCode() {
return editor.hashCode();
}
@Override
public String toString() {
return "PklSettings{" + "editor=" + editor + '}';
}
/** An editor for viewing and editing Pkl files. */
public static final class Editor {
private final String urlScheme;
public record Editor(String urlScheme) {
/** The editor associated with {@code file:} URLs ending in {@code .pkl}. */
public static final Editor SYSTEM = new Editor("%{url}, line %{line}");
@@ -134,37 +120,15 @@ public final class PklSettings {
/** The <a href="https://code.visualstudio.com">Visual Studio Code</a> editor. */
public static final Editor VS_CODE = new Editor("vscode://file/%{path}:%{line}:%{column}");
/** Constructs an editor. */
public Editor(String urlScheme) {
this.urlScheme = urlScheme;
}
/**
* Returns the URL scheme for opening files in this editor. The following placeholders are
* supported: {@code %{url}}, {@code %{path}}, {@code %{line}}, {@code %{column}}.
*
* <p>This method is deprecated; use {@link #urlScheme()} instead.
*/
@Deprecated(forRemoval = true)
public String getUrlScheme() {
return urlScheme;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
var editor = (Editor) o;
return urlScheme.equals(editor.urlScheme);
}
@Override
public int hashCode() {
return urlScheme.hashCode();
}
@Override
public String toString() {
return "Editor{" + "urlScheme='" + urlScheme + '\'' + '}';
}
}
}

View File

@@ -540,6 +540,18 @@ public final class IoUtils {
System.setProperty("org.pkl.testMode", "true");
}
public static void setSystemProxy(URI proxyAddress) {
// Set HTTP proxy settings to configure the certificate revocation checker, because
// there is no other way to configure it. (see https://bugs.openjdk.org/browse/JDK-8256409)
//
// This only influences the behavior of the revocation checker.
// Otherwise, proxying is handled by [ProxySelector].
System.setProperty("http.proxyHost", proxyAddress.getHost());
System.setProperty(
"http.proxyPort",
proxyAddress.getPort() == -1 ? "80" : String.valueOf(proxyAddress.getPort()));
}
public static @Nullable String parseTripleDotPath(URI importUri) throws URISyntaxException {
var importScheme = importUri.getScheme();
if (importScheme != null) return null;

View File

@@ -1053,3 +1053,7 @@ Certificates can only be loaded from `jar:` or `file:` URLs, but got:\n\
cannotFindBuiltInCertificates=\
Cannot find Pkl's trusted CA certificates on the class path.\n\
To fix this problem, add dependendy `org.pkl:pkl-certs`.
# suppress inspection "HttpUrlsUsage"
malformedProxyAddress=\
Malformed proxy URI (expecting `http://<host>[:<port>]`): `{0}`.

View File

@@ -10,6 +10,7 @@ pkl:base
pkl:Benchmark
pkl:DocPackageInfo
pkl:DocsiteInfo
pkl:EvaluatorSettings
pkl:json
pkl:jsonnet
pkl:math

View File

@@ -0,0 +1,146 @@
package org.pkl.core.http
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.net.URI
@Suppress("HttpUrlsUsage")
class NoProxyRuleTest {
@Test
fun wildcard() {
val noProxyRule = NoProxyRule("*")
assertTrue(noProxyRule.matches(URI("https://foo.com")))
assertTrue(noProxyRule.matches(URI("https://bar.com")))
assertTrue(noProxyRule.matches(URI("https://foo:5000")))
}
@Test
fun `hostname matching`() {
val noProxyRule = NoProxyRule("foo.com")
assertTrue(noProxyRule.matches(URI("https://foo.com")))
assertTrue(noProxyRule.matches(URI("http://foo.com")))
assertTrue(noProxyRule.matches(URI("https://foo.com:5000")))
assertTrue(noProxyRule.matches(URI("https://FOO.COM")))
assertTrue(noProxyRule.matches(URI("https://bar.foo.com")))
assertFalse(noProxyRule.matches(URI("https://bar.foo.com.bar")))
assertFalse(noProxyRule.matches(URI("https://bar.foocom")))
assertFalse(noProxyRule.matches(URI("https://fooo.com")))
assertFalse(noProxyRule.matches(URI("https://ooofoo.com")))
assertFalse(noProxyRule.matches(URI("pkl:foo.com")))
}
@Test
fun `hostname matching, leading dot`() {
val noProxyRule = NoProxyRule(".foo.com")
assertTrue(noProxyRule.matches(URI("https://foo.com")))
assertTrue(noProxyRule.matches(URI("http://foo.com")))
assertTrue(noProxyRule.matches(URI("https://foo.com:5000")))
assertTrue(noProxyRule.matches(URI("https://FOO.COM")))
assertTrue(noProxyRule.matches(URI("https://bar.foo.com")))
assertFalse(noProxyRule.matches(URI("https://bar.foo.com.bar")))
assertFalse(noProxyRule.matches(URI("https://bar.foocom")))
assertFalse(noProxyRule.matches(URI("https://fooo.com")))
assertFalse(noProxyRule.matches(URI("https://ooofoo.com")))
assertFalse(noProxyRule.matches(URI("pkl:foo.com")))
}
@Test
fun `hostname matching, with port`() {
val noProxyRule = NoProxyRule("foo.com:5000")
assertTrue(noProxyRule.matches(URI("https://foo.com:5000")))
assertFalse(noProxyRule.matches(URI("https://foo.com")))
assertFalse(noProxyRule.matches(URI("https://foo.com:3000")))
}
@Test
fun `ipv4 address literal matching`() {
val noProxyRule = NoProxyRule("192.168.1.1")
assertTrue(noProxyRule.matches(URI("http://192.168.1.1:5000")))
assertTrue(noProxyRule.matches(URI("http://192.168.1.1")))
assertTrue(noProxyRule.matches(URI("https://192.168.1.1")))
assertFalse(noProxyRule.matches(URI("https://192.168.1.0")))
assertFalse(noProxyRule.matches(URI("https://192.168.1.0:5000")))
}
@Test
fun `ipv4 address literal matching, with port`() {
val noProxyRule = NoProxyRule("192.168.1.1:5000")
assertTrue(noProxyRule.matches(URI("http://192.168.1.1:5000")))
assertTrue(noProxyRule.matches(URI("https://192.168.1.1:5000")))
assertFalse(noProxyRule.matches(URI("http://192.168.1.1")))
assertFalse(noProxyRule.matches(URI("https://192.168.1.1")))
assertFalse(noProxyRule.matches(URI("http://192.168.1.1:3000")))
assertFalse(noProxyRule.matches(URI("https://192.168.1.1:3000")))
}
@Test
fun `ipv6 address literal matching`() {
val noProxyRule = NoProxyRule("::1")
assertTrue(noProxyRule.matches(URI("http://[::1]")))
assertTrue(noProxyRule.matches(URI("http://[::1]:5000")))
assertTrue(noProxyRule.matches(URI("https://[::1]")))
assertTrue(noProxyRule.matches(URI("https://[0000:0000:0000:0000:0000:0000:0000:0001]")))
assertFalse(noProxyRule.matches(URI("https://[::2]")))
assertFalse(noProxyRule.matches(URI("https://[::2]:5000")))
}
@Test
fun `ipv6 address literal matching, with port`() {
val noProxyRule = NoProxyRule("[::1]:5000")
assertTrue(noProxyRule.matches(URI("http://[::1]:5000")))
assertTrue(noProxyRule.matches(URI("https://[0000:0000:0000:0000:0000:0000:0000:0001]:5000")))
assertFalse(noProxyRule.matches(URI("http://[::1]")))
assertFalse(noProxyRule.matches(URI("https://[::1]")))
assertFalse(noProxyRule.matches(URI("https://[::2]")))
assertFalse(noProxyRule.matches(URI("https://[::2]:5000")))
}
@Test
fun `ipv4 port from protocol`() {
val noProxyRuleHttp = NoProxyRule("192.168.1.1:80")
assertTrue(noProxyRuleHttp.matches(URI("http://192.168.1.1")))
assertTrue(noProxyRuleHttp.matches(URI("http://192.168.1.1:80")))
assertTrue(noProxyRuleHttp.matches(URI("https://192.168.1.1:80")))
assertFalse(noProxyRuleHttp.matches(URI("https://192.168.1.1")))
assertFalse(noProxyRuleHttp.matches(URI("https://192.168.1.1:5000")))
val noProxyRuleHttps = NoProxyRule("192.168.1.1:443")
assertTrue(noProxyRuleHttps.matches(URI("https://192.168.1.1")))
assertTrue(noProxyRuleHttps.matches(URI("http://192.168.1.1:443")))
assertFalse(noProxyRuleHttps.matches(URI("http://192.168.1.1")))
assertFalse(noProxyRuleHttps.matches(URI("https://192.168.1.1:80")))
}
@Test
fun `ipv4 cidr block matching`() {
val noProxyRule1 = NoProxyRule("10.0.0.0/16")
assertTrue(noProxyRule1.matches(URI("https://10.0.0.0")))
assertTrue(noProxyRule1.matches(URI("https://10.0.255.255")))
assertTrue(noProxyRule1.matches(URI("https://10.0.255.255:5000")))
assertFalse(noProxyRule1.matches(URI("https://10.1.0.0")))
assertFalse(noProxyRule1.matches(URI("https://11.0.0.0")))
assertFalse(noProxyRule1.matches(URI("https://9.255.255.255")))
assertFalse(noProxyRule1.matches(URI("https://9.255.255.255:5000")))
val noProxyRule2 = NoProxyRule("10.0.0.0/32")
assertTrue(noProxyRule2.matches(URI("https://10.0.0.0")))
assertTrue(noProxyRule2.matches(URI("https://10.0.0.0:5000")))
assertFalse(noProxyRule2.matches(URI("https://10.0.0.1")))
assertFalse(noProxyRule2.matches(URI("https://9.255.255.55:5000")))
}
@Test
fun `ipv6 cidr block matching`() {
val noProxyRule1 = NoProxyRule("1000::ff/32")
assertTrue(noProxyRule1.matches(URI("https://[1000::]")))
assertTrue(noProxyRule1.matches(URI("https://[1000:0:ffff:ffff:ffff:ffff:ffff:ffff]")))
assertFalse(noProxyRule1.matches(URI("https://[999::]")))
assertFalse(noProxyRule1.matches(URI("https://[1000:1::]")))
val noProxyRule2 = NoProxyRule("1000::ff/128")
assertTrue(noProxyRule2.matches(URI("https://[1000::ff]")))
assertFalse(noProxyRule2.matches(URI("https://[999::]")))
assertFalse(noProxyRule2.matches(URI("https://[1001::]")))
}
}

View File

@@ -6,7 +6,7 @@ import java.net.http.HttpResponse
class RequestCapturingClient : HttpClient {
lateinit var request: HttpRequest
override fun <T : Any> send(
request: HttpRequest,
responseBodyHandler: HttpResponse.BodyHandler<T>

View File

@@ -10,7 +10,7 @@ import org.pkl.commons.writeString
import org.pkl.core.*
import org.pkl.core.http.HttpClient
import org.pkl.core.packages.PackageUri
import org.pkl.core.project.Project.EvaluatorSettings
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
import java.net.URI
import java.nio.file.Path
import java.util.regex.Pattern
@@ -40,7 +40,7 @@ class ProjectTest {
listOf(Path.of("apiTest1.pkl"), Path.of("apiTest2.pkl")),
listOf("PklProject", "PklProject.deps.json", ".**", "*.exe")
)
val expectedSettings = EvaluatorSettings(
val expectedSettings = PklEvaluatorSettings(
mapOf("two" to "2"),
mapOf("one" to "1"),
listOf("foo:", "bar:").map(Pattern::compile),
@@ -52,7 +52,8 @@ class ProjectTest {
path.resolve("modulepath2/")
),
Duration.ofMinutes(5.0),
path
path,
null
)
projectPath.writeString("""
amends "pkl:Project"
@@ -116,7 +117,7 @@ class ProjectTest {
""".trimIndent())
val project = Project.loadFromPath(projectPath)
assertThat(project.`package`).isEqualTo(expectedPackage)
assertThat(project.settings).isEqualTo(expectedSettings)
assertThat(project.evaluatorSettings).isEqualTo(expectedSettings)
assertThat(project.tests).isEqualTo(listOf(path.resolve("test1.pkl"), path.resolve("test2.pkl")))
}

View File

@@ -4,13 +4,18 @@ import java.nio.file.Path
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.createParentDirectories
import org.pkl.commons.writeString
import org.pkl.core.Evaluator
import org.pkl.core.ModuleSource
import org.pkl.core.PObject
import org.pkl.core.StackFrameTransformers
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
import org.pkl.core.runtime.VmException
import org.pkl.core.settings.PklSettings.Editor
import java.net.URI
class PklSettingsTest {
@Test
@@ -25,7 +30,61 @@ class PklSettingsTest {
)
val settings = PklSettings.loadFromPklHomeDir(tempDir)
assertThat(settings).isEqualTo(PklSettings(Editor.SUBLIME))
assertThat(settings).isEqualTo(PklSettings(Editor.SUBLIME, null))
}
@Test
fun `load user settings with http`(@TempDir tempDir: Path) {
val settingsPath = tempDir.resolve("settings.pkl")
settingsPath.createParentDirectories()
settingsPath.writeString(
"""
amends "pkl:settings"
http {
proxy {
address = "http://localhost:8080"
noProxy {
"example.com"
"pkg.pkl-lang.org"
}
}
}
""".trimIndent()
)
val settings = PklSettings.loadFromPklHomeDir(tempDir)
val expectedHttp = PklEvaluatorSettings.Http(
PklEvaluatorSettings.Proxy(
URI("http://localhost:8080"),
listOf("example.com", "pkg.pkl-lang.org")
)
)
assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp))
}
@Test
fun `load user settings with http, but no noProxy`(@TempDir tempDir: Path) {
val settingsPath = tempDir.resolve("settings.pkl")
settingsPath.createParentDirectories()
settingsPath.writeString(
"""
amends "pkl:settings"
http {
proxy {
address = "http://localhost:8080"
}
}
""".trimIndent()
)
val settings = PklSettings.loadFromPklHomeDir(tempDir)
val expectedHttp = PklEvaluatorSettings.Http(
PklEvaluatorSettings.Proxy(
URI("http://localhost:8080"),
listOf(),
)
)
assertThat(settings).isEqualTo(PklSettings(Editor.SYSTEM, expectedHttp))
}
@Test
@@ -39,7 +98,7 @@ class PklSettingsTest {
)
val settings = PklSettings.load(ModuleSource.path(settingsPath))
assertThat(settings).isEqualTo(PklSettings(Editor.IDEA))
assertThat(settings).isEqualTo(PklSettings(Editor.IDEA, null))
}
@Test
@@ -76,6 +135,6 @@ class PklSettingsTest {
}
private fun checkEquals(expected: Editor, actual: PObject) {
assertThat(actual.getProperty("urlScheme") as String).isEqualTo(expected.urlScheme)
assertThat(actual.getProperty("urlScheme") as String).isEqualTo(expected.urlScheme())
}
}