mirror of
https://github.com/apple/pkl.git
synced 2026-04-19 15:01:26 +02:00
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:
committed by
GitHub
parent
a520ae7d04
commit
b03530ed1f
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NonnullByDefault
|
||||
package org.pkl.core.evaluatorSettings;
|
||||
|
||||
import org.pkl.core.util.NonnullByDefault;
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
215
pkl-core/src/main/java/org/pkl/core/http/NoProxyRule.java
Normal file
215
pkl-core/src/main/java/org/pkl/core/http/NoProxyRule.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
pkl-core/src/main/java/org/pkl/core/http/ProxySelector.java
Normal file
78
pkl-core/src/main/java/org/pkl/core/http/ProxySelector.java
Normal 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 */
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 + '\'' + '}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`.
|
||||
|
||||
@@ -10,6 +10,7 @@ pkl:base
|
||||
pkl:Benchmark
|
||||
pkl:DocPackageInfo
|
||||
pkl:DocsiteInfo
|
||||
pkl:EvaluatorSettings
|
||||
pkl:json
|
||||
pkl:jsonnet
|
||||
pkl:math
|
||||
|
||||
146
pkl-core/src/test/kotlin/org/pkl/core/http/NoProxyRuleTest.kt
Normal file
146
pkl-core/src/test/kotlin/org/pkl/core/http/NoProxyRuleTest.kt
Normal 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::]")))
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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")))
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user