mirror of
https://github.com/apple/pkl.git
synced 2026-03-27 19:41:18 +01:00
Use java.net.http.HttpClient instead of java.net.Http(s)URLConnection (#217)
Moving to java.net.http.HttpClient brings many benefits, including HTTP/2 support and the ability to make asynchronous requests. Major additions and changes: - Introduce a lightweight org.pkl.core.http.HttpClient API. This keeps some flexibility and allows to enforce behavior such as setting the User-Agent header. - Provide an implementation that delegates to java.net.http.HttpClient. - Use HttpClient for all HTTP(s) requests across the codebase. This required adding an HttpClient parameter to constructors and factory methods of multiple classes, some of which are public APIs. - Manage CA certificates per HTTP client instead of per JVM. This makes it unnecessary to set JVM-wide system/security properties and default SSLSocketFactory's. - Add executor v2 options to the executor SPI - Add pkl-certs as a new artifact, and remove certs from pkl-commons-cli artifact Each HTTP client maintains its own connection pool and SSLContext. For efficiency reasons, It's best to reuse clients whenever feasible. To avoid memory leaks, clients are not stored in static fields. HTTP clients are expensive to create. For this reason, EvaluatorBuilder defaults to a "lazy" client that creates the underlying java.net.http.HttpClient on the first send (which may never happen).
This commit is contained in:
@@ -20,6 +20,7 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import org.pkl.core.SecurityManagers.StandardBuilder;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.ModuleKeyFactories;
|
||||
import org.pkl.core.module.ModuleKeyFactory;
|
||||
import org.pkl.core.module.ModulePathResolver;
|
||||
@@ -38,6 +39,10 @@ public final class EvaluatorBuilder {
|
||||
|
||||
private @Nullable SecurityManager securityManager;
|
||||
|
||||
// Default to a client with a fixed set of built-in certificates.
|
||||
// Make it lazy to avoid creating a client unnecessarily.
|
||||
private HttpClient httpClient = HttpClient.builder().buildLazily();
|
||||
|
||||
private Logger logger = Loggers.noop();
|
||||
|
||||
private final List<ModuleKeyFactory> moduleKeyFactories = new ArrayList<>();
|
||||
@@ -226,6 +231,21 @@ public final class EvaluatorBuilder {
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP client to be used.
|
||||
*
|
||||
* <p>Defaults to {@code HttpClient.builder().buildLazily()}.
|
||||
*/
|
||||
public EvaluatorBuilder setHttpClient(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns the currently set HTTP client. */
|
||||
public HttpClient getHttpClient() {
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given module key factory. Factories will be asked to resolve module keys in the order
|
||||
* they have been added to this builder.
|
||||
@@ -468,6 +488,7 @@ public final class EvaluatorBuilder {
|
||||
return new EvaluatorImpl(
|
||||
stackFrameTransformer,
|
||||
securityManager,
|
||||
httpClient,
|
||||
new LoggerImpl(logger, stackFrameTransformer),
|
||||
// copy to shield against subsequent modification through builder
|
||||
new ArrayList<>(moduleKeyFactories),
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.function.Supplier;
|
||||
import org.graalvm.polyglot.Context;
|
||||
import org.pkl.core.ast.ConstantValueNode;
|
||||
import org.pkl.core.ast.internal.ToStringNodeGen;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.ModuleKeyFactory;
|
||||
import org.pkl.core.module.ProjectDependenciesManager;
|
||||
import org.pkl.core.packages.PackageResolver;
|
||||
@@ -69,6 +70,7 @@ public class EvaluatorImpl implements Evaluator {
|
||||
public EvaluatorImpl(
|
||||
StackFrameTransformer transformer,
|
||||
SecurityManager manager,
|
||||
HttpClient httpClient,
|
||||
Logger logger,
|
||||
Collection<ModuleKeyFactory> factories,
|
||||
Collection<ResourceReader> readers,
|
||||
@@ -83,7 +85,7 @@ public class EvaluatorImpl implements Evaluator {
|
||||
frameTransformer = transformer;
|
||||
moduleResolver = new ModuleResolver(factories);
|
||||
this.logger = new BufferedLogger(logger);
|
||||
packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir);
|
||||
packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir);
|
||||
polyglotContext =
|
||||
VmUtils.createContext(
|
||||
() -> {
|
||||
@@ -92,6 +94,7 @@ public class EvaluatorImpl implements Evaluator {
|
||||
new VmContext.Holder(
|
||||
transformer,
|
||||
manager,
|
||||
httpClient,
|
||||
moduleResolver,
|
||||
new ResourceManager(manager, readers),
|
||||
this.logger,
|
||||
|
||||
@@ -23,4 +23,8 @@ public class PklException extends RuntimeException {
|
||||
public PklException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PklException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.ast.VmModifier;
|
||||
import org.pkl.core.ast.member.ObjectMember;
|
||||
import org.pkl.core.ast.member.UntypedObjectMemberNode;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.runtime.BaseModule;
|
||||
@@ -113,7 +114,7 @@ public class ImportGlobNode extends AbstractImportNode {
|
||||
frame.materialize(), BaseModule.getMappingClass().getPrototype(), members);
|
||||
} catch (IOException e) {
|
||||
throw exceptionBuilder().evalError("ioErrorResolvingGlob", importUri).withCause(e).build();
|
||||
} catch (SecurityManagerException e) {
|
||||
} catch (SecurityManagerException | HttpClientInitException e) {
|
||||
throw exceptionBuilder().withCause(e).build();
|
||||
} catch (PackageLoadError e) {
|
||||
throw exceptionBuilder().adhocEvalError(e.getMessage()).build();
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.oracle.truffle.api.nodes.NodeInfo;
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
import java.net.URI;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
@@ -60,7 +61,7 @@ public final class ImportNode extends AbstractImportNode {
|
||||
context.getSecurityManager().checkImportModule(currentModule.getUri(), importUri);
|
||||
var moduleToImport = context.getModuleResolver().resolve(importUri, this);
|
||||
importedModule = language.loadModule(moduleToImport, this);
|
||||
} catch (SecurityManagerException | PackageLoadError e) {
|
||||
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
|
||||
throw exceptionBuilder().withCause(e).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.http;
|
||||
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandler;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/** An {@code HttpClient} implementation that throws {@code AssertionError} on every send. */
|
||||
@ThreadSafe
|
||||
final class DummyHttpClient implements HttpClient {
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler) {
|
||||
throw new AssertionError("Dummy HTTP client cannot send request: " + request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
}
|
||||
182
pkl-core/src/main/java/org/pkl/core/http/HttpClient.java
Normal file
182
pkl-core/src/main/java/org/pkl/core/http/HttpClient.java
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpTimeoutException;
|
||||
import java.nio.file.Path;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
/**
|
||||
* An HTTP client.
|
||||
*
|
||||
* <p>To create a new HTTP client, use a {@linkplain #builder() builder}. To send {@linkplain
|
||||
* HttpRequest requests} and retrieve their {@linkplain HttpResponse responses}, use {@link #send}.
|
||||
* To release resources held by the client, use {@link #close}.
|
||||
*
|
||||
* <p>HTTP clients are thread-safe. Each client maintains its own connection pool and {@link
|
||||
* SSLContext}. For efficiency reasons, clients should be reused whenever possible.
|
||||
*/
|
||||
public interface HttpClient extends AutoCloseable {
|
||||
|
||||
/** A builder of {@linkplain HttpClient HTTP clients}. */
|
||||
interface Builder {
|
||||
/**
|
||||
* Sets the {@code User-Agent} header.
|
||||
*
|
||||
* <p>Defaults to {@code "Pkl/$version ($os; $flavor)"}.
|
||||
*/
|
||||
Builder setUserAgent(String userAgent);
|
||||
|
||||
/**
|
||||
* Sets the timeout for connecting to a server.
|
||||
*
|
||||
* <p>Defaults to 60 seconds.
|
||||
*/
|
||||
Builder setConnectTimeout(java.time.Duration timeout);
|
||||
|
||||
/**
|
||||
* Sets the timeout for the interval between sending a request and receiving response headers.
|
||||
*
|
||||
* <p>Defaults to 60 seconds. To set a timeout for a specific request, use {@link
|
||||
* HttpRequest.Builder#timeout}.
|
||||
*/
|
||||
Builder setRequestTimeout(java.time.Duration timeout);
|
||||
|
||||
/**
|
||||
* Adds a CA certificate file to the client's trust store.
|
||||
*
|
||||
* <p>The given file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
||||
* certificates in PEM format.
|
||||
*/
|
||||
Builder addCertificates(Path file);
|
||||
|
||||
/**
|
||||
* Adds a CA certificate file to the client's trust store.
|
||||
*
|
||||
* <p>The given file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
||||
* certificates in PEM format.
|
||||
*
|
||||
* <p>This method is intended to be used for adding certificate files located on the class path.
|
||||
* To add certificate files located on the file system, use {@link #addCertificates(Path)}.
|
||||
*
|
||||
* @throws HttpClientInitException if the given URI has a scheme other than {@code jar:} or
|
||||
* {@code file:}
|
||||
*/
|
||||
Builder addCertificates(URI file);
|
||||
|
||||
/**
|
||||
* Adds the CA certificate files in {@code ~/.pkl/cacerts/} to the client's trust store.
|
||||
*
|
||||
* <p>Each file must contain <a href="https://en.wikipedia.org/wiki/X.509">X.509</a>
|
||||
* certificates in PEM format. If {@code ~/.pkl/cacerts/} does not exist or is empty, Pkl's
|
||||
* {@link #addBuiltInCertificates() built-in certificates} are added instead.
|
||||
*
|
||||
* <p>This method implements the default behavior of Pkl CLIs.
|
||||
*
|
||||
* <p>NOTE: This method requires the optional {@code pkl-certs} JAR to be present on the class
|
||||
* path.
|
||||
*
|
||||
* @throws HttpClientInitException if an I/O error occurs while scanning {@code ~/.pkl/cacerts/}
|
||||
* or the {@code pkl-certs} JAR is not found on the class path
|
||||
*/
|
||||
Builder addDefaultCliCertificates();
|
||||
|
||||
/**
|
||||
* Adds Pkl's built-in CA certificates to the client's trust store.
|
||||
*
|
||||
* <p>NOTE: This method requires the optional {@code pkl-certs} JAR to be present on the class
|
||||
* path.
|
||||
*
|
||||
* @throws HttpClientInitException if the {@code pkl-certs} JAR is not found on the class path
|
||||
*/
|
||||
Builder addBuiltInCertificates();
|
||||
|
||||
/**
|
||||
* Creates a new {@code HttpClient} from the current state of this builder.
|
||||
*
|
||||
* @throws HttpClientInitException if an error occurs while initializing the client
|
||||
*/
|
||||
HttpClient build();
|
||||
|
||||
/**
|
||||
* Returns an {@code HTTPClient} wrapper that defers building the actual HTTP client until the
|
||||
* wrapper's {@link HttpClient#send} method is called.
|
||||
*
|
||||
* <p>Note: When using this method, any exception thrown when building the actual HTTP client is
|
||||
* equally deferred.
|
||||
*/
|
||||
HttpClient buildLazily();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code HTTPClient} builder with default settings.
|
||||
*
|
||||
* <p>The default settings are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Connect timeout: 60 seconds
|
||||
* <li>Request timeout: 60 seconds
|
||||
* <li>CA certificates: none (falls back to the JVM's {@linkplain SSLContext#getDefault()
|
||||
* default SSL context})
|
||||
* </ul>
|
||||
*/
|
||||
static Builder builder() {
|
||||
return new HttpClientBuilder();
|
||||
}
|
||||
|
||||
/** Returns a client that throws {@link AssertionError} on every attempt to send a request. */
|
||||
static HttpClient dummyClient() {
|
||||
return new DummyHttpClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP request. The response body is processed by the given body handler.
|
||||
*
|
||||
* <p>If the request does not specify a {@linkplain HttpRequest#timeout timeout}, the client's
|
||||
* {@linkplain Builder#setRequestTimeout request timeout} is used. If the request does not specify
|
||||
* a preferred {@linkplain HttpRequest#version() HTTP version}, HTTP/2 is used. The request's
|
||||
* {@code User-Agent} header is set to the client's {@link Builder#setUserAgent User-Agent}
|
||||
* header.
|
||||
*
|
||||
* <p>Depending on the given body handler, this method blocks until response headers or the entire
|
||||
* response body has been received. If response headers are not received within the request
|
||||
* timeout, {@link HttpTimeoutException} is thrown.
|
||||
*
|
||||
* <p>For additional information on how to use this method, see {@link
|
||||
* java.net.http.HttpClient#send}.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs when sending or receiving
|
||||
* @throws HttpClientInitException if an error occurs while initializing a {@linkplain
|
||||
* Builder#buildLazily lazy} client
|
||||
*/
|
||||
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Closes this client.
|
||||
*
|
||||
* <p>This method makes a best effort to release the resources held by this client in a timely
|
||||
* manner. This may involve waiting for pending requests to complete.
|
||||
*
|
||||
* <p>Subsequent calls to this method have no effect. Subsequent calls to any other method throw
|
||||
* {@link IllegalStateException}.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
135
pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java
Normal file
135
pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import org.pkl.core.Release;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
|
||||
final class HttpClientBuilder implements HttpClient.Builder {
|
||||
private final Path userHome;
|
||||
private String userAgent;
|
||||
private Duration connectTimeout = Duration.ofSeconds(60);
|
||||
private Duration requestTimeout = Duration.ofSeconds(60);
|
||||
private final List<Path> certificateFiles = new ArrayList<>();
|
||||
private final List<URI> certificateUris = new ArrayList<>();
|
||||
|
||||
HttpClientBuilder() {
|
||||
this(Path.of(System.getProperty("user.home")));
|
||||
}
|
||||
|
||||
// only exists for testing
|
||||
HttpClientBuilder(Path userHome) {
|
||||
this.userHome = userHome;
|
||||
var release = Release.current();
|
||||
userAgent = "Pkl/" + release.version() + " (" + release.os() + "; " + release.flavor() + ")";
|
||||
}
|
||||
|
||||
public HttpClient.Builder setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient.Builder setConnectTimeout(Duration timeout) {
|
||||
this.connectTimeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient.Builder setRequestTimeout(Duration timeout) {
|
||||
this.requestTimeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient.Builder addCertificates(Path file) {
|
||||
certificateFiles.add(file);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient.Builder addCertificates(URI url) {
|
||||
var scheme = url.getScheme();
|
||||
if (!"jar".equalsIgnoreCase(scheme) && !"file".equalsIgnoreCase(scheme)) {
|
||||
throw new HttpClientInitException(ErrorMessages.create("expectedJarOrFileUrl", url));
|
||||
}
|
||||
certificateUris.add(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClient.Builder addDefaultCliCertificates() {
|
||||
var directory = userHome.resolve(".pkl").resolve("cacerts");
|
||||
var fileCount = certificateFiles.size();
|
||||
if (Files.isDirectory(directory)) {
|
||||
try (var files = Files.list(directory)) {
|
||||
files.filter(Files::isRegularFile).forEach(certificateFiles::add);
|
||||
} catch (IOException e) {
|
||||
throw new HttpClientInitException(e);
|
||||
}
|
||||
}
|
||||
if (certificateFiles.size() == fileCount) {
|
||||
addBuiltInCertificates();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient.Builder addBuiltInCertificates() {
|
||||
certificateUris.add(getBuiltInCertificates());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient build() {
|
||||
return doBuild().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient buildLazily() {
|
||||
return new LazyHttpClient(doBuild());
|
||||
}
|
||||
|
||||
private Supplier<HttpClient> doBuild() {
|
||||
// make defensive copies because Supplier may get called after builder was mutated
|
||||
var certificateFiles = List.copyOf(this.certificateFiles);
|
||||
var certificateUris = List.copyOf(this.certificateUris);
|
||||
return () -> {
|
||||
var jdkClient = new JdkHttpClient(certificateFiles, certificateUris, connectTimeout);
|
||||
return new RequestRewritingClient(userAgent, requestTimeout, jdkClient);
|
||||
};
|
||||
}
|
||||
|
||||
private static URI getBuiltInCertificates() {
|
||||
var resource = HttpClientBuilder.class.getResource("/org/pkl/certs/PklCARoots.pem");
|
||||
if (resource == null) {
|
||||
throw new HttpClientInitException(ErrorMessages.create("cannotFindBuiltInCertificates"));
|
||||
}
|
||||
try {
|
||||
return resource.toURI();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new AssertionError("unreachable");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.http;
|
||||
|
||||
import org.pkl.core.PklException;
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred while initializing an HTTP client. A common example is an error
|
||||
* reading or parsing a certificate.
|
||||
*/
|
||||
public class HttpClientInitException extends PklException {
|
||||
public HttpClientInitException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HttpClientInitException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public HttpClientInitException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
206
pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java
Normal file
206
pkl-core/src/main/java/org/pkl/core/http/JdkHttpClient.java
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandler;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertPathBuilder;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.PKIXBuilderParameters;
|
||||
import java.security.cert.PKIXRevocationChecker;
|
||||
import java.security.cert.TrustAnchor;
|
||||
import java.security.cert.X509CertSelector;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.net.ssl.CertPathTrustManagerParameters;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.Exceptions;
|
||||
|
||||
/** An {@code HttpClient} implementation backed by {@link java.net.http.HttpClient}. */
|
||||
@ThreadSafe
|
||||
final class JdkHttpClient implements HttpClient {
|
||||
// non-private for testing
|
||||
final java.net.http.HttpClient underlying;
|
||||
|
||||
// call java.net.http.HttpClient.close() if available (JDK 21+)
|
||||
private static final MethodHandle closeMethod;
|
||||
|
||||
static {
|
||||
var methodType = MethodType.methodType(void.class, java.net.http.HttpClient.class);
|
||||
MethodHandle result;
|
||||
try {
|
||||
//noinspection JavaLangInvokeHandleSignature
|
||||
result =
|
||||
MethodHandles.publicLookup()
|
||||
.findVirtual(java.net.http.HttpClient.class, "close", methodType);
|
||||
} catch (NoSuchMethodException | IllegalAccessException e) {
|
||||
// use no-op close method
|
||||
result = MethodHandles.empty(methodType);
|
||||
}
|
||||
closeMethod = result;
|
||||
}
|
||||
|
||||
JdkHttpClient(List<Path> certificateFiles, List<URI> certificateUris, Duration connectTimeout) {
|
||||
underlying =
|
||||
java.net.http.HttpClient.newBuilder()
|
||||
.sslContext(createSslContext(certificateFiles, certificateUris))
|
||||
.connectTimeout(connectTimeout)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||
throws IOException {
|
||||
try {
|
||||
return underlying.send(request, responseBodyHandler);
|
||||
} catch (ConnectException e) {
|
||||
// original exception has no message
|
||||
throw new ConnectException(
|
||||
ErrorMessages.create("errorConnectingToHost", request.uri().getHost()));
|
||||
} catch (SSLHandshakeException e) {
|
||||
throw new SSLHandshakeException(
|
||||
ErrorMessages.create(
|
||||
"errorSslHandshake", request.uri().getHost(), Exceptions.getRootReason(e)));
|
||||
} catch (SSLException e) {
|
||||
throw new SSLException(Exceptions.getRootReason(e));
|
||||
} catch (IOException e) {
|
||||
// JDK 11 throws IOException instead of SSLHandshakeException
|
||||
throw new IOException(Exceptions.getRootReason(e));
|
||||
} catch (InterruptedException e) {
|
||||
// next best thing after letting (checked) InterruptedException bubble up
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
closeMethod.invoke(underlying);
|
||||
} catch (RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable t) {
|
||||
throw new AssertionError(t);
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#security-algorithm-implementation-requirements
|
||||
private static SSLContext createSslContext(
|
||||
List<Path> certificateFiles, List<URI> certificateUris) {
|
||||
try {
|
||||
if (certificateFiles.isEmpty() && certificateUris.isEmpty()) {
|
||||
// fall back to JVM defaults (not Pkl built-in certs)
|
||||
return SSLContext.getDefault();
|
||||
}
|
||||
|
||||
var certPathBuilder = CertPathBuilder.getInstance("PKIX");
|
||||
// create a non-legacy revocation checker that is configured via setOptions() instead of
|
||||
// security property "ocsp.enabled"
|
||||
var revocationChecker = (PKIXRevocationChecker) certPathBuilder.getRevocationChecker();
|
||||
revocationChecker.setOptions(Set.of()); // prefer OCSP, fall back to CRLs
|
||||
|
||||
var certFactory = CertificateFactory.getInstance("X.509");
|
||||
Set<TrustAnchor> trustAnchors =
|
||||
createTrustAnchors(certFactory, certificateFiles, certificateUris);
|
||||
var pkixParameters = new PKIXBuilderParameters(trustAnchors, new X509CertSelector());
|
||||
// equivalent of "com.sun.net.ssl.checkRevocation=true"
|
||||
pkixParameters.setRevocationEnabled(true);
|
||||
pkixParameters.addCertPathChecker(revocationChecker);
|
||||
|
||||
var trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
|
||||
trustManagerFactory.init(new CertPathTrustManagerParameters(pkixParameters));
|
||||
|
||||
var sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
|
||||
|
||||
return sslContext;
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new HttpClientInitException(
|
||||
ErrorMessages.create("cannotInitHttpClient", Exceptions.getRootReason(e)), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<TrustAnchor> createTrustAnchors(
|
||||
CertificateFactory factory, List<Path> certificateFiles, List<URI> certificateUris) {
|
||||
var anchors = new HashSet<TrustAnchor>();
|
||||
|
||||
for (var file : certificateFiles) {
|
||||
try (var stream = Files.newInputStream(file)) {
|
||||
collectTrustAnchors(anchors, factory, stream, file);
|
||||
} catch (NoSuchFileException e) {
|
||||
throw new HttpClientInitException(ErrorMessages.create("cannotFindCertFile", file));
|
||||
} catch (IOException e) {
|
||||
throw new HttpClientInitException(
|
||||
ErrorMessages.create("cannotReadCertFile", Exceptions.getRootReason(e)));
|
||||
}
|
||||
}
|
||||
|
||||
for (var uri : certificateUris) {
|
||||
try (var stream = uri.toURL().openStream()) {
|
||||
collectTrustAnchors(anchors, factory, stream, uri);
|
||||
} catch (IOException e) {
|
||||
throw new HttpClientInitException(
|
||||
ErrorMessages.create("cannotReadCertFile", Exceptions.getRootReason(e)));
|
||||
}
|
||||
}
|
||||
|
||||
return anchors;
|
||||
}
|
||||
|
||||
private static void collectTrustAnchors(
|
||||
Collection<TrustAnchor> anchors,
|
||||
CertificateFactory factory,
|
||||
InputStream stream,
|
||||
Object source) {
|
||||
Collection<X509Certificate> certificates;
|
||||
try {
|
||||
//noinspection unchecked
|
||||
certificates = (Collection<X509Certificate>) factory.generateCertificates(stream);
|
||||
} catch (CertificateException e) {
|
||||
throw new HttpClientInitException(
|
||||
ErrorMessages.create("cannotParseCertFile", source, Exceptions.getRootReason(e)));
|
||||
}
|
||||
if (certificates.isEmpty()) {
|
||||
throw new HttpClientInitException(ErrorMessages.create("emptyCertFile", source));
|
||||
}
|
||||
for (var certificate : certificates) {
|
||||
anchors.add(new TrustAnchor(certificate, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
81
pkl-core/src/main/java/org/pkl/core/http/LazyHttpClient.java
Normal file
81
pkl-core/src/main/java/org/pkl/core/http/LazyHttpClient.java
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandler;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* An {@code HttpClient} decorator that defers creating the underlying HTTP client until the first
|
||||
* send.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class LazyHttpClient implements HttpClient {
|
||||
private final Supplier<HttpClient> supplier;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
private HttpClient client;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private RuntimeException exception;
|
||||
|
||||
LazyHttpClient(Supplier<HttpClient> supplier) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||
throws IOException {
|
||||
return getOrCreateClient().send(request, responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
getClient().ifPresent(HttpClient::close);
|
||||
}
|
||||
|
||||
private HttpClient getOrCreateClient() {
|
||||
synchronized (lock) {
|
||||
// only try to create client once
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
if (client == null) {
|
||||
try {
|
||||
client = supplier.get();
|
||||
} catch (RuntimeException t) {
|
||||
exception = t;
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<HttpClient> getClient() {
|
||||
synchronized (lock) {
|
||||
return Optional.ofNullable(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandler;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/**
|
||||
* An {@code HttpClient} decorator that
|
||||
*
|
||||
* <ul>
|
||||
* <li>overrides the {@code User-Agent} header of {@code HttpRequest}s
|
||||
* <li>sets a request timeout if none is present
|
||||
* <li>ensures that {@link #close()} is idempotent.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Both {@code User-Agent} header and default request timeout are configurable through {@link
|
||||
* HttpClient.Builder}.
|
||||
*/
|
||||
@ThreadSafe
|
||||
final class RequestRewritingClient implements HttpClient {
|
||||
// non-private for testing
|
||||
final String userAgent;
|
||||
final Duration requestTimeout;
|
||||
final HttpClient delegate;
|
||||
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
RequestRewritingClient(String userAgent, Duration requestTimeout, HttpClient delegate) {
|
||||
this.userAgent = userAgent;
|
||||
this.requestTimeout = requestTimeout;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||
throws IOException {
|
||||
checkNotClosed(request);
|
||||
return delegate.send(rewriteRequest(request), responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed.getAndSet(true)) delegate.close();
|
||||
}
|
||||
|
||||
// Based on JDK 17's implementation of HttpRequest.newBuilder(HttpRequest, filter).
|
||||
private HttpRequest rewriteRequest(HttpRequest original) {
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder();
|
||||
|
||||
builder
|
||||
.uri(original.uri())
|
||||
.expectContinue(original.expectContinue())
|
||||
.timeout(original.timeout().orElse(requestTimeout))
|
||||
.version(original.version().orElse(java.net.http.HttpClient.Version.HTTP_2));
|
||||
|
||||
original
|
||||
.headers()
|
||||
.map()
|
||||
.forEach((name, values) -> values.forEach(value -> builder.header(name, value)));
|
||||
builder.setHeader("User-Agent", userAgent);
|
||||
|
||||
var method = original.method();
|
||||
original
|
||||
.bodyPublisher()
|
||||
.ifPresentOrElse(
|
||||
publisher -> builder.method(method, publisher),
|
||||
() -> {
|
||||
switch (method) {
|
||||
case "GET":
|
||||
builder.GET();
|
||||
break;
|
||||
case "DELETE":
|
||||
builder.DELETE();
|
||||
break;
|
||||
default:
|
||||
builder.method(method, HttpRequest.BodyPublishers.noBody());
|
||||
}
|
||||
});
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void checkNotClosed(HttpRequest request) {
|
||||
if (closed.get()) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot send request " + request + " because this client has already been closed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
@@ -34,6 +36,7 @@ import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.packages.PackageResolver;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.HttpUtils;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
import org.pkl.core.util.Nullable;
|
||||
import org.pkl.core.util.Pair;
|
||||
@@ -481,6 +484,19 @@ public final class ModuleKeys {
|
||||
throws IOException, SecurityManagerException {
|
||||
securityManager.checkResolveModule(uri);
|
||||
|
||||
if (HttpUtils.isHttpUrl(uri)) {
|
||||
var httpClient = VmContext.get(null).getHttpClient();
|
||||
var request = HttpRequest.newBuilder(uri).build();
|
||||
var response = httpClient.send(request, BodyHandlers.ofInputStream());
|
||||
try (var body = response.body()) {
|
||||
HttpUtils.checkHasStatusCode200(response);
|
||||
securityManager.checkResolveModule(response.uri());
|
||||
String text = IoUtils.readString(body);
|
||||
// intentionally use uri instead of response.uri()
|
||||
return ResolvedModuleKeys.virtual(this, uri, text, true);
|
||||
}
|
||||
}
|
||||
|
||||
var url = IoUtils.toUrl(uri);
|
||||
var conn = url.openConnection();
|
||||
conn.connect();
|
||||
@@ -494,6 +510,7 @@ public final class ModuleKeys {
|
||||
}
|
||||
securityManager.checkResolveModule(redirected);
|
||||
var text = IoUtils.readString(stream);
|
||||
// intentionally use uri instead of redirected
|
||||
return ResolvedModuleKeys.virtual(this, uri, text, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,13 @@ package org.pkl.core.module;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
import org.pkl.core.util.HttpUtils;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
|
||||
/** Utilities for obtaining and using resolved module keys. */
|
||||
@@ -102,6 +106,14 @@ public final class ResolvedModuleKeys {
|
||||
|
||||
@Override
|
||||
public String loadSource() throws IOException {
|
||||
if (HttpUtils.isHttpUrl(url)) {
|
||||
var httpClient = VmContext.get(null).getHttpClient();
|
||||
var request = HttpRequest.newBuilder(uri).build();
|
||||
var response = httpClient.send(request, BodyHandlers.ofString());
|
||||
HttpUtils.checkHasStatusCode200(response);
|
||||
return response.body();
|
||||
}
|
||||
|
||||
return IoUtils.readString(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.List;
|
||||
import javax.naming.OperationNotSupportedException;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.PathElement;
|
||||
import org.pkl.core.packages.PackageResolvers.DiskCachedPackageResolver;
|
||||
import org.pkl.core.packages.PackageResolvers.InMemoryPackageResolver;
|
||||
@@ -30,10 +31,11 @@ import org.pkl.core.util.Pair;
|
||||
|
||||
public interface PackageResolver extends Closeable {
|
||||
|
||||
static PackageResolver getInstance(SecurityManager securityManager, @Nullable Path cachedDir) {
|
||||
static PackageResolver getInstance(
|
||||
SecurityManager securityManager, HttpClient httpClient, @Nullable Path cachedDir) {
|
||||
return cachedDir == null
|
||||
? new InMemoryPackageResolver(securityManager)
|
||||
: new DiskCachedPackageResolver(securityManager, cachedDir);
|
||||
? new InMemoryPackageResolver(securityManager, httpClient)
|
||||
: new DiskCachedPackageResolver(securityManager, httpClient, cachedDir);
|
||||
}
|
||||
|
||||
DependencyMetadata getDependencyMetadata(PackageUri uri, @Nullable Checksums checksums)
|
||||
|
||||
@@ -21,6 +21,9 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystem;
|
||||
@@ -41,11 +44,10 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import org.graalvm.collections.EconomicMap;
|
||||
import org.pkl.core.Release;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.FileResolver;
|
||||
import org.pkl.core.module.PathElement;
|
||||
import org.pkl.core.module.PathElement.TreePathElement;
|
||||
@@ -53,6 +55,7 @@ import org.pkl.core.runtime.FileSystemManager;
|
||||
import org.pkl.core.runtime.VmExceptionBuilder;
|
||||
import org.pkl.core.util.ByteArrayUtils;
|
||||
import org.pkl.core.util.EconomicMaps;
|
||||
import org.pkl.core.util.HttpUtils;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
import org.pkl.core.util.Nullable;
|
||||
import org.pkl.core.util.Pair;
|
||||
@@ -60,24 +63,20 @@ import org.pkl.core.util.json.Json.JsonParseException;
|
||||
|
||||
class PackageResolvers {
|
||||
abstract static class AbstractPackageResolver implements PackageResolver {
|
||||
private static final String USER_AGENT;
|
||||
|
||||
static {
|
||||
var release = Release.current();
|
||||
USER_AGENT = "Pkl/" + release.version() + " (" + release.os() + "; " + release.flavor() + ")";
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private final EconomicMap<PackageUri, DependencyMetadata> cachedDependencyMetadata;
|
||||
|
||||
private final SecurityManager securityManager;
|
||||
|
||||
protected final HttpClient httpClient;
|
||||
|
||||
private final AtomicBoolean isClosed = new AtomicBoolean();
|
||||
|
||||
protected final Object lock = new Object();
|
||||
|
||||
protected AbstractPackageResolver(SecurityManager securityManager) {
|
||||
protected AbstractPackageResolver(SecurityManager securityManager, HttpClient httpClient) {
|
||||
this.securityManager = securityManager;
|
||||
this.httpClient = httpClient;
|
||||
cachedDependencyMetadata = EconomicMaps.create();
|
||||
}
|
||||
|
||||
@@ -189,21 +188,26 @@ class PackageResolvers {
|
||||
}
|
||||
}
|
||||
|
||||
protected InputStream openExternalUri(URI uri) throws SecurityManagerException, IOException {
|
||||
protected InputStream openExternalUri(URI uri) throws SecurityManagerException {
|
||||
if (!HttpUtils.isHttpUrl(uri)) {
|
||||
throw new IllegalArgumentException("Expected HTTP(S) URL, but got: " + uri);
|
||||
}
|
||||
|
||||
// treat package assets as resources instead of modules
|
||||
securityManager.checkReadResource(uri);
|
||||
var connection = (HttpsURLConnection) uri.toURL().openConnection();
|
||||
connection.setRequestProperty("User-Agent", USER_AGENT);
|
||||
int responseCode;
|
||||
var request = HttpRequest.newBuilder(uri).build();
|
||||
HttpResponse<InputStream> response;
|
||||
try {
|
||||
responseCode = connection.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
throw new PackageLoadError("badHttpStatusCode", responseCode, uri);
|
||||
}
|
||||
response = httpClient.send(request, BodyHandlers.ofInputStream());
|
||||
} catch (IOException e) {
|
||||
throw new PackageLoadError(e, "ioErrorMakingHttpGet", uri, e.getMessage());
|
||||
}
|
||||
return connection.getInputStream();
|
||||
try {
|
||||
HttpUtils.checkHasStatusCode200(response);
|
||||
} catch (IOException e) {
|
||||
throw new PackageLoadError("badHttpStatusCode", response.statusCode(), response.uri());
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
|
||||
protected IOException fileIsADirectory() {
|
||||
@@ -251,8 +255,8 @@ class PackageResolvers {
|
||||
private final EconomicMap<PackageUri, TreePathElement> cachedTreePathElementRoots =
|
||||
EconomicMaps.create();
|
||||
|
||||
InMemoryPackageResolver(SecurityManager securityManager) {
|
||||
super(securityManager);
|
||||
InMemoryPackageResolver(SecurityManager securityManager, HttpClient httpClient) {
|
||||
super(securityManager, httpClient);
|
||||
}
|
||||
|
||||
private byte[] getPackageBytes(PackageUri packageUri, DependencyMetadata metadata)
|
||||
@@ -419,8 +423,9 @@ class PackageResolvers {
|
||||
PosixFilePermission.GROUP_READ,
|
||||
PosixFilePermission.OTHERS_READ);
|
||||
|
||||
public DiskCachedPackageResolver(SecurityManager securityManager, Path cacheDir) {
|
||||
super(securityManager);
|
||||
public DiskCachedPackageResolver(
|
||||
SecurityManager securityManager, HttpClient httpClient, Path cacheDir) {
|
||||
super(securityManager, httpClient);
|
||||
this.cacheDir = cacheDir;
|
||||
this.tmpDir = cacheDir.resolve("tmp");
|
||||
}
|
||||
@@ -463,8 +468,8 @@ class PackageResolvers {
|
||||
if (checksums != null) {
|
||||
inputStream = newDigestInputStream(inputStream);
|
||||
}
|
||||
Files.createDirectories(path.getParent());
|
||||
try (var in = inputStream) {
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.copy(in, path);
|
||||
if (checksums != null) {
|
||||
var digestInputStream = (DigestInputStream) inputStream;
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.StackFrameTransformer;
|
||||
import org.pkl.core.ast.builder.ImportsAndReadsParser;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.ModuleKeys;
|
||||
import org.pkl.core.module.ProjectDependenciesManager;
|
||||
import org.pkl.core.module.ResolvedModuleKeys;
|
||||
@@ -105,6 +106,7 @@ public class ProjectPackager {
|
||||
String outputPathPattern,
|
||||
StackFrameTransformer stackFrameTransformer,
|
||||
SecurityManager securityManager,
|
||||
HttpClient httpClient,
|
||||
boolean skipPublishCheck,
|
||||
Writer outputWriter) {
|
||||
this.projects = projects;
|
||||
@@ -112,7 +114,7 @@ public class ProjectPackager {
|
||||
this.outputPathPattern = outputPathPattern;
|
||||
this.stackFrameTransformer = stackFrameTransformer;
|
||||
// intentionally use InMemoryPackageResolver
|
||||
this.packageResolver = PackageResolver.getInstance(securityManager, null);
|
||||
this.packageResolver = PackageResolver.getInstance(securityManager, httpClient, null);
|
||||
this.skipPublishCheck = skipPublishCheck;
|
||||
this.outputWriter = outputWriter;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.pkl.core.ast.builder.AstBuilder;
|
||||
import org.pkl.core.ast.member.*;
|
||||
import org.pkl.core.ast.repl.ResolveClassMemberNode;
|
||||
import org.pkl.core.ast.type.TypeNode;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.*;
|
||||
import org.pkl.core.packages.PackageResolver;
|
||||
import org.pkl.core.parser.LexParseException;
|
||||
@@ -67,6 +68,7 @@ public class ReplServer implements AutoCloseable {
|
||||
|
||||
public ReplServer(
|
||||
SecurityManager securityManager,
|
||||
HttpClient httpClient,
|
||||
Logger logger,
|
||||
Collection<ModuleKeyFactory> moduleKeyFactories,
|
||||
Collection<ResourceReader> resourceReaders,
|
||||
@@ -85,7 +87,7 @@ public class ReplServer implements AutoCloseable {
|
||||
replState = new ReplState(createEmptyReplModule(BaseModule.getModuleClass().getPrototype()));
|
||||
|
||||
var languageRef = new MutableReference<VmLanguage>(null);
|
||||
packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir);
|
||||
packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir);
|
||||
projectDependenciesManager =
|
||||
projectDependencies == null ? null : new ProjectDependenciesManager(projectDependencies);
|
||||
polyglotContext =
|
||||
@@ -97,6 +99,7 @@ public class ReplServer implements AutoCloseable {
|
||||
new VmContext.Holder(
|
||||
frameTransformer,
|
||||
securityManager,
|
||||
httpClient,
|
||||
moduleResolver,
|
||||
new ResourceManager(securityManager, resourceReaders),
|
||||
logger,
|
||||
|
||||
@@ -19,6 +19,8 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
@@ -38,6 +40,7 @@ import org.pkl.core.packages.PackageAssetUri;
|
||||
import org.pkl.core.packages.PackageResolver;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.HttpUtils;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
@@ -291,6 +294,15 @@ public final class ResourceReaders {
|
||||
private abstract static class UrlResource implements ResourceReader {
|
||||
@Override
|
||||
public Optional<Object> read(URI uri) throws IOException {
|
||||
if (HttpUtils.isHttpUrl(uri)) {
|
||||
var httpClient = VmContext.get(null).getHttpClient();
|
||||
var request = HttpRequest.newBuilder(uri).build();
|
||||
var response = httpClient.send(request, BodyHandlers.ofByteArray());
|
||||
if (response.statusCode() == 404) return Optional.empty();
|
||||
HttpUtils.checkHasStatusCode200(response);
|
||||
return Optional.of(new Resource(uri, response.body()));
|
||||
}
|
||||
|
||||
try {
|
||||
var url = IoUtils.toUrl(uri);
|
||||
var content = IoUtils.readBytes(url);
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.runtime;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
public class CertificateUtils {
|
||||
public static void setupAllX509CertificatesGlobally(List<Object> certs) {
|
||||
try {
|
||||
var certificates = new ArrayList<X509Certificate>(certs.size());
|
||||
for (var cert : certs) {
|
||||
try (var input = toInputStream(cert)) {
|
||||
certificates.addAll(generateCertificates(input));
|
||||
}
|
||||
}
|
||||
setupX509CertificatesGlobally(certificates);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream toInputStream(Object cert) throws IOException {
|
||||
if (cert instanceof Path) {
|
||||
var pathCert = (Path) cert;
|
||||
return Files.newInputStream(pathCert);
|
||||
}
|
||||
if (cert instanceof InputStream) {
|
||||
return (InputStream) cert;
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown class for certificate: "
|
||||
+ cert.getClass()
|
||||
+ ". Valid types: java.nio.Path, java.io.InputStream");
|
||||
}
|
||||
|
||||
private static Collection<X509Certificate> generateCertificates(InputStream inputStream)
|
||||
throws CertificateException {
|
||||
//noinspection unchecked
|
||||
return (Collection<X509Certificate>)
|
||||
CertificateFactory.getInstance("X.509").generateCertificates(inputStream);
|
||||
}
|
||||
|
||||
private static void setupX509CertificatesGlobally(Collection<X509Certificate> certs)
|
||||
throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException,
|
||||
KeyManagementException {
|
||||
System.setProperty("com.sun.net.ssl.checkRevocation", "true");
|
||||
Security.setProperty("ocsp.enable", "true");
|
||||
var keystore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keystore.load(null);
|
||||
|
||||
var count = 1;
|
||||
for (var cert : certs) {
|
||||
keystore.setCertificateEntry("Certificate" + count++, cert);
|
||||
}
|
||||
var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(keystore);
|
||||
|
||||
var sc = SSLContext.getInstance("SSL");
|
||||
sc.init(null, tmf.getTrustManagers(), new SecureRandom());
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.resource.Resource;
|
||||
@@ -105,7 +106,7 @@ public final class ResourceManager {
|
||||
.withHint(e.getMessage())
|
||||
.withLocation(readNode)
|
||||
.build();
|
||||
} catch (SecurityManagerException e) {
|
||||
} catch (SecurityManagerException | HttpClientInitException e) {
|
||||
throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build();
|
||||
} catch (IOException e) {
|
||||
throw new VmExceptionBuilder()
|
||||
@@ -151,7 +152,7 @@ public final class ResourceManager {
|
||||
.withHint(e.getReason())
|
||||
.withLocation(readNode)
|
||||
.build();
|
||||
} catch (SecurityManagerException | PackageLoadError e) {
|
||||
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
|
||||
throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build();
|
||||
}
|
||||
if (resource.isEmpty()) return resource;
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.Map;
|
||||
import org.pkl.core.Loggers;
|
||||
import org.pkl.core.SecurityManagers;
|
||||
import org.pkl.core.StackFrameTransformers;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.ModuleKeyFactories;
|
||||
import org.pkl.core.module.ModuleKeys;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
@@ -39,6 +40,7 @@ public abstract class StdLibModule {
|
||||
new VmContext.Holder(
|
||||
StackFrameTransformers.defaultTransformer,
|
||||
SecurityManagers.defaultManager,
|
||||
HttpClient.dummyClient(),
|
||||
new ModuleResolver(List.of(ModuleKeyFactories.standardLibrary)),
|
||||
new ResourceManager(SecurityManagers.defaultManager, List.of()),
|
||||
Loggers.noop(),
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Map;
|
||||
import org.pkl.core.Logger;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.StackFrameTransformer;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.ProjectDependenciesManager;
|
||||
import org.pkl.core.packages.PackageResolver;
|
||||
import org.pkl.core.util.LateInit;
|
||||
@@ -39,6 +40,7 @@ public final class VmContext {
|
||||
|
||||
private final StackFrameTransformer frameTransformer;
|
||||
private final SecurityManager securityManager;
|
||||
private final HttpClient httpClient;
|
||||
private final ModuleResolver moduleResolver;
|
||||
private final ResourceManager resourceManager;
|
||||
private final Logger logger;
|
||||
@@ -52,6 +54,7 @@ public final class VmContext {
|
||||
public Holder(
|
||||
StackFrameTransformer frameTransformer,
|
||||
SecurityManager securityManager,
|
||||
HttpClient httpClient,
|
||||
ModuleResolver moduleResolver,
|
||||
ResourceManager resourceManager,
|
||||
Logger logger,
|
||||
@@ -64,6 +67,7 @@ public final class VmContext {
|
||||
|
||||
this.frameTransformer = frameTransformer;
|
||||
this.securityManager = securityManager;
|
||||
this.httpClient = httpClient;
|
||||
this.moduleResolver = moduleResolver;
|
||||
this.resourceManager = resourceManager;
|
||||
this.logger = logger;
|
||||
@@ -108,6 +112,10 @@ public final class VmContext {
|
||||
return holder.securityManager;
|
||||
}
|
||||
|
||||
public HttpClient getHttpClient() {
|
||||
return holder.httpClient;
|
||||
}
|
||||
|
||||
public ModuleResolver getModuleResolver() {
|
||||
return holder.moduleResolver;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,19 @@ package org.pkl.core.service;
|
||||
|
||||
import static org.pkl.core.module.ProjectDependenciesManager.PKL_PROJECT_FILENAME;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import org.pkl.core.*;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.ModuleKeyFactories;
|
||||
import org.pkl.core.module.ModulePathResolver;
|
||||
import org.pkl.core.project.Project;
|
||||
@@ -28,10 +37,30 @@ import org.pkl.core.resource.ResourceReaders;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpi;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiException;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
|
||||
|
||||
public class ExecutorSpiImpl implements ExecutorSpi {
|
||||
private static final int MAX_HTTP_CLIENTS = 3;
|
||||
|
||||
// Don't create a new HTTP client for every executor request.
|
||||
// Instead, keep a cache of up to MAX_HTTP_CLIENTS clients.
|
||||
// A cache size of 1 should be common.
|
||||
private final Map<HttpClientKey, HttpClient> httpClients;
|
||||
|
||||
private final String pklVersion = Release.current().version().toString();
|
||||
|
||||
public ExecutorSpiImpl() {
|
||||
// only LRU cache available in JDK
|
||||
var map =
|
||||
new LinkedHashMap<HttpClientKey, HttpClient>(8, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Entry<HttpClientKey, HttpClient> eldest) {
|
||||
return size() > MAX_HTTP_CLIENTS;
|
||||
}
|
||||
};
|
||||
httpClients = Collections.synchronizedMap(map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPklVersion() {
|
||||
return pklVersion;
|
||||
@@ -65,6 +94,7 @@ public class ExecutorSpiImpl implements ExecutorSpi {
|
||||
EvaluatorBuilder.unconfigured()
|
||||
.setStackFrameTransformer(transformer)
|
||||
.setSecurityManager(securityManager)
|
||||
.setHttpClient(getOrCreateHttpClient(options))
|
||||
.addResourceReader(ResourceReaders.environmentVariable())
|
||||
.addResourceReader(ResourceReaders.externalProperty())
|
||||
.addResourceReader(ResourceReaders.modulePath(resolver))
|
||||
@@ -98,4 +128,61 @@ public class ExecutorSpiImpl implements ExecutorSpi {
|
||||
ModuleKeyFactories.closeQuietly(builder.getModuleKeyFactories());
|
||||
}
|
||||
}
|
||||
|
||||
private HttpClient getOrCreateHttpClient(ExecutorSpiOptions options) {
|
||||
List<Path> certificateFiles;
|
||||
List<URI> certificateUris;
|
||||
if (options instanceof ExecutorSpiOptions2) {
|
||||
var options2 = (ExecutorSpiOptions2) options;
|
||||
certificateFiles = options2.getCertificateFiles();
|
||||
certificateUris = options2.getCertificateUris();
|
||||
} else {
|
||||
certificateFiles = List.of();
|
||||
certificateUris = List.of();
|
||||
}
|
||||
var clientKey = new HttpClientKey(certificateFiles, certificateUris);
|
||||
return httpClients.computeIfAbsent(
|
||||
clientKey,
|
||||
(key) -> {
|
||||
var builder = HttpClient.builder();
|
||||
for (var file : key.certificateFiles) {
|
||||
builder.addCertificates(file);
|
||||
}
|
||||
for (var uri : key.certificateUris) {
|
||||
builder.addCertificates(uri);
|
||||
}
|
||||
// If the above didn't add any certificates,
|
||||
// builder will use the JVM's default SSL context.
|
||||
return builder.buildLazily();
|
||||
});
|
||||
}
|
||||
|
||||
private static final class HttpClientKey {
|
||||
final Set<Path> certificateFiles;
|
||||
final Set<URI> certificateUris;
|
||||
|
||||
HttpClientKey(List<Path> certificateFiles, List<URI> certificateUris) {
|
||||
// also serve as defensive copies
|
||||
this.certificateFiles = Set.copyOf(certificateFiles);
|
||||
this.certificateUris = Set.copyOf(certificateUris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
HttpClientKey that = (HttpClientKey) obj;
|
||||
return certificateFiles.equals(that.certificateFiles)
|
||||
&& certificateUris.equals(that.certificateUris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(certificateFiles, certificateUris);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
pkl-core/src/main/java/org/pkl/core/util/Exceptions.java
Normal file
36
pkl-core/src/main/java/org/pkl/core/util/Exceptions.java
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.util;
|
||||
|
||||
public final class Exceptions {
|
||||
private Exceptions() {}
|
||||
|
||||
public static Throwable getRootCause(Throwable t) {
|
||||
var result = t;
|
||||
var cause = result.getCause();
|
||||
while (cause != null) {
|
||||
result = cause;
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String getRootReason(Throwable t) {
|
||||
var reason = getRootCause(t).getMessage();
|
||||
if (reason == null || reason.isEmpty()) reason = "(unknown reason)";
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
50
pkl-core/src/main/java/org/pkl/core/util/HttpUtils.java
Normal file
50
pkl-core/src/main/java/org/pkl/core/util/HttpUtils.java
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
public final class HttpUtils {
|
||||
private HttpUtils() {}
|
||||
|
||||
public static boolean isHttpUrl(URL url) {
|
||||
var protocol = url.getProtocol();
|
||||
return "https".equalsIgnoreCase(protocol) || "http".equalsIgnoreCase(protocol);
|
||||
}
|
||||
|
||||
public static boolean isHttpUrl(URI uri) {
|
||||
var scheme = uri.getScheme();
|
||||
return "https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme);
|
||||
}
|
||||
|
||||
public static void checkHasStatusCode200(HttpResponse<?> response) throws IOException {
|
||||
if (response.statusCode() == 200) return;
|
||||
|
||||
var body = response.body();
|
||||
if (body instanceof AutoCloseable) {
|
||||
try {
|
||||
((AutoCloseable) body).close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException(
|
||||
ErrorMessages.create("badHttpStatusCode", response.statusCode(), response.uri()));
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,9 @@ public final class IoUtils {
|
||||
}
|
||||
|
||||
public static String readString(URL url) throws IOException {
|
||||
if (HttpUtils.isHttpUrl(url)) {
|
||||
throw new IllegalArgumentException("Should use HTTP client to GET " + url);
|
||||
}
|
||||
try (var stream = url.openStream()) {
|
||||
return readString(stream);
|
||||
}
|
||||
@@ -110,6 +113,9 @@ public final class IoUtils {
|
||||
}
|
||||
|
||||
public static byte[] readBytes(URL url) throws IOException {
|
||||
if (HttpUtils.isHttpUrl(url)) {
|
||||
throw new IllegalArgumentException("Should use HTTP client to GET " + url);
|
||||
}
|
||||
try (var stream = url.openStream()) {
|
||||
return stream.readAllBytes();
|
||||
}
|
||||
|
||||
@@ -940,6 +940,31 @@ ioErrorMakingHttpGet=\
|
||||
Exception when making request `GET {0}`:\n\
|
||||
{1}
|
||||
|
||||
errorConnectingToHost=\
|
||||
Error connecting to host `{0}`.
|
||||
|
||||
errorSslHandshake=\
|
||||
Error during SSL handshake with host `{0}`:\n\
|
||||
{1}
|
||||
|
||||
cannotInitHttpClient=\
|
||||
Error initializing HTTP client:\n\
|
||||
{0}
|
||||
|
||||
cannotFindCertFile=\
|
||||
Cannot find CA certificate file `{0}`.
|
||||
|
||||
cannotReadCertFile=\
|
||||
Error reading CA certificate file `{0}`:\n\
|
||||
{1}
|
||||
|
||||
cannotParseCertFile=\
|
||||
Error parsing CA certificate file `{0}`:\n\
|
||||
{1}
|
||||
|
||||
emptyCertFile=\
|
||||
CA certificate file `{0}` is empty.
|
||||
|
||||
invalidPackageZipUrl=\
|
||||
Expected the zip asset for package `{0}` to be an HTTPS URI, but got `{1}`.
|
||||
|
||||
@@ -1019,3 +1044,11 @@ The only supported checksum algorithm is sha256.
|
||||
|
||||
testsFailed=\
|
||||
Tests failed.
|
||||
|
||||
expectedJarOrFileUrl=\
|
||||
Certificates can only be loaded from `jar:` or `file:` URLs, but got:\n\
|
||||
{0}
|
||||
|
||||
cannotFindBuiltInCertificates=\
|
||||
Cannot find Pkl's trusted CA certificates on the class path.\n\
|
||||
To fix this problem, add dependendy `org.pkl:pkl-certs`.
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.pkl.core
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.pkl.commons.toPath
|
||||
import org.pkl.core.http.HttpClient
|
||||
import org.pkl.core.module.ModuleKeyFactories
|
||||
import org.pkl.core.repl.ReplRequest
|
||||
import org.pkl.core.repl.ReplResponse
|
||||
@@ -12,6 +13,7 @@ import org.pkl.core.resource.ResourceReaders
|
||||
class ReplServerTest {
|
||||
private val server = ReplServer(
|
||||
SecurityManagers.defaultManager,
|
||||
HttpClient.dummyClient(),
|
||||
Loggers.stdErr(),
|
||||
listOf(
|
||||
ModuleKeyFactories.standardLibrary,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.pkl.core.http
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.net.URI
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
|
||||
class DummyHttpClientTest {
|
||||
@Test
|
||||
fun `refuses to send messages`() {
|
||||
val client = HttpClient.dummyClient()
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
|
||||
|
||||
assertThrows<AssertionError> {
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||
}
|
||||
|
||||
assertThrows<AssertionError> {
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can be closed`() {
|
||||
val client = HttpClient.dummyClient()
|
||||
|
||||
assertDoesNotThrow {
|
||||
client.close()
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
148
pkl-core/src/test/kotlin/org/pkl/core/http/HttpClientTest.kt
Normal file
148
pkl-core/src/test/kotlin/org/pkl/core/http/HttpClientTest.kt
Normal file
@@ -0,0 +1,148 @@
|
||||
package org.pkl.core.http
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.pkl.commons.test.FileTestUtils
|
||||
import org.pkl.core.Release
|
||||
import java.net.URI
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.createFile
|
||||
|
||||
class HttpClientTest {
|
||||
@Test
|
||||
fun `can build default client`() {
|
||||
val client = assertDoesNotThrow {
|
||||
HttpClient.builder().build()
|
||||
}
|
||||
|
||||
assertThat(client).isInstanceOf(RequestRewritingClient::class.java)
|
||||
client as RequestRewritingClient
|
||||
|
||||
val release = Release.current()
|
||||
assertThat(client.userAgent).isEqualTo("Pkl/${release.version()} (${release.os()}; ${release.flavor()})")
|
||||
assertThat(client.requestTimeout).isEqualTo(Duration.ofSeconds(60))
|
||||
|
||||
assertThat(client.delegate).isInstanceOf(JdkHttpClient::class.java)
|
||||
val delegate = client.delegate as JdkHttpClient
|
||||
|
||||
assertThat(delegate.underlying.connectTimeout()).hasValue(Duration.ofSeconds(60))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can build custom client`() {
|
||||
val client = HttpClient.builder()
|
||||
.setUserAgent("Agent 1")
|
||||
.setRequestTimeout(Duration.ofHours(86))
|
||||
.setConnectTimeout(Duration.ofMinutes(42))
|
||||
.build() as RequestRewritingClient
|
||||
|
||||
assertThat(client.userAgent).isEqualTo("Agent 1")
|
||||
assertThat(client.requestTimeout).isEqualTo(Duration.ofHours(86))
|
||||
|
||||
val delegate = client.delegate as JdkHttpClient
|
||||
assertThat(delegate.underlying.connectTimeout()).hasValue(Duration.ofMinutes(42))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load certificates from file system`() {
|
||||
assertDoesNotThrow {
|
||||
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate).build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `certificate file located on file system cannot be empty`(@TempDir tempDir: Path) {
|
||||
val file = tempDir.resolve("certs.pem").createFile()
|
||||
|
||||
val e = assertThrows<HttpClientInitException> {
|
||||
HttpClient.builder().addCertificates(file).build()
|
||||
}
|
||||
|
||||
assertThat(e).hasMessageContaining("empty")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load certificates from class path`() {
|
||||
assertDoesNotThrow {
|
||||
HttpClient.builder().addCertificates(javaClass.getResource("/org/pkl/certs/PklCARoots.pem")!!.toURI()).build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only allows loading jar and file certificate URIs`() {
|
||||
assertThrows<HttpClientInitException> {
|
||||
HttpClient.builder().addCertificates(URI("https://example.com"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `certificate file located on class path cannot be empty`() {
|
||||
val uri = javaClass.getResource("emptyCerts.pem")!!.toURI()
|
||||
|
||||
val e = assertThrows<HttpClientInitException> {
|
||||
HttpClient.builder().addCertificates(uri).build()
|
||||
}
|
||||
|
||||
assertThat(e).hasMessageContaining("empty")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load built-in certificates`() {
|
||||
assertDoesNotThrow {
|
||||
HttpClient.builder().addBuiltInCertificates().build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load certificates from Pkl user home cacerts directory`(@TempDir tempDir: Path) {
|
||||
val certFile = tempDir.resolve(".pkl")
|
||||
.resolve("cacerts")
|
||||
.createDirectories()
|
||||
.resolve("certs.pem")
|
||||
FileTestUtils.selfSignedCertificate.copyTo(certFile)
|
||||
|
||||
assertDoesNotThrow {
|
||||
HttpClientBuilder(tempDir).addDefaultCliCertificates().build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loading certificates from cacerts directory falls back to built-in certificates`(@TempDir userHome: Path) {
|
||||
assertDoesNotThrow {
|
||||
HttpClientBuilder(userHome).addDefaultCliCertificates().build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can be closed multiple times`() {
|
||||
val client = HttpClient.builder().build()
|
||||
|
||||
assertDoesNotThrow {
|
||||
client.close()
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refuses to send messages once closed`() {
|
||||
val client = HttpClient.builder().build()
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
|
||||
|
||||
client.close()
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||
}
|
||||
assertThrows<IllegalStateException> {
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.pkl.core.http
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.net.URI
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse.BodyHandlers
|
||||
|
||||
class LazyHttpClientTest {
|
||||
@Test
|
||||
fun `builds underlying client on first send`() {
|
||||
val client = HttpClient.builder()
|
||||
.addCertificates(javaClass.getResource("brokenCerts.pem")!!.toURI())
|
||||
.buildLazily()
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
|
||||
|
||||
assertThrows<HttpClientInitException> {
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not build underlying client unnecessarily`() {
|
||||
val client = HttpClient.builder()
|
||||
.addCertificates(javaClass.getResource("brokenCerts.pem")!!.toURI())
|
||||
.buildLazily()
|
||||
|
||||
assertDoesNotThrow {
|
||||
client.close()
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.pkl.core.http
|
||||
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
|
||||
class RequestCapturingClient : HttpClient {
|
||||
lateinit var request: HttpRequest
|
||||
|
||||
override fun <T : Any?> send(
|
||||
request: HttpRequest,
|
||||
responseBodyHandler: HttpResponse.BodyHandler<T>
|
||||
): HttpResponse<T>? {
|
||||
this.request = request
|
||||
return null
|
||||
}
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.pkl.core.http
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatList
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.URI
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpRequest.BodyPublishers
|
||||
import java.net.http.HttpResponse
|
||||
import java.net.http.HttpResponse.BodyHandlers
|
||||
import java.time.Duration
|
||||
import java.net.http.HttpClient as JdkHttpClient
|
||||
|
||||
class RequestRewritingClientTest {
|
||||
private val captured = RequestCapturingClient()
|
||||
private val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), captured)
|
||||
private val exampleUri = URI("https://example.com/foo/bar.html")
|
||||
private val exampleRequest = HttpRequest.newBuilder(exampleUri).build()
|
||||
|
||||
@Test
|
||||
fun `fills in missing User-Agent header`() {
|
||||
client.send(exampleRequest, BodyHandlers.discarding())
|
||||
|
||||
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("Pkl")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `overrides existing User-Agent headers`() {
|
||||
val request = HttpRequest.newBuilder(exampleUri)
|
||||
.header("User-Agent", "Agent 1")
|
||||
.header("User-Agent", "Agent 2")
|
||||
.build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
|
||||
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("Pkl")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fills in missing request timeout`() {
|
||||
client.send(exampleRequest, BodyHandlers.discarding())
|
||||
|
||||
assertThat(captured.request.timeout()).hasValue(Duration.ofSeconds(42))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leaves existing request timeout intact`() {
|
||||
val request = HttpRequest.newBuilder(exampleUri)
|
||||
.timeout(Duration.ofMinutes(33))
|
||||
.build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
|
||||
assertThat(captured.request.timeout()).hasValue(Duration.ofMinutes(33))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fills in missing HTTP version`() {
|
||||
client.send(exampleRequest, BodyHandlers.discarding())
|
||||
|
||||
assertThat(captured.request.version()).hasValue(JdkHttpClient.Version.HTTP_2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leaves existing HTTP version intact`() {
|
||||
val request = HttpRequest.newBuilder(exampleUri)
|
||||
.version(JdkHttpClient.Version.HTTP_1_1)
|
||||
.build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
|
||||
assertThat(captured.request.version()).hasValue(JdkHttpClient.Version.HTTP_1_1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leaves default method intact`() {
|
||||
val request = HttpRequest.newBuilder(exampleUri).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
|
||||
assertThat(captured.request.method()).isEqualTo("GET")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leaves explicit method intact`() {
|
||||
val request = HttpRequest.newBuilder(exampleUri)
|
||||
.DELETE()
|
||||
.build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
|
||||
assertThat(captured.request.method()).isEqualTo("DELETE")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leaves body publisher intact`() {
|
||||
val publisher = BodyPublishers.ofString("body")
|
||||
val request = HttpRequest.newBuilder(exampleUri)
|
||||
.PUT(publisher)
|
||||
.build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
|
||||
assertThat(captured.request.bodyPublisher().get()).isSameAs(publisher)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ import org.pkl.commons.readString
|
||||
import org.pkl.commons.test.FileTestUtils
|
||||
import org.pkl.commons.test.PackageServer
|
||||
import org.pkl.commons.test.listFilesRecursively
|
||||
import org.pkl.core.http.HttpClient
|
||||
import org.pkl.core.SecurityManagers
|
||||
import org.pkl.core.module.PathElement
|
||||
import org.pkl.core.runtime.CertificateUtils
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -34,9 +34,12 @@ class PackageResolversTest {
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun beforeAll() {
|
||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
||||
PackageServer.ensureStarted()
|
||||
}
|
||||
|
||||
val httpClient: HttpClient = HttpClient.builder()
|
||||
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -196,16 +199,17 @@ class PackageResolversTest {
|
||||
@BeforeAll
|
||||
@JvmStatic
|
||||
fun beforeAll() {
|
||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
||||
PackageServer.ensureStarted()
|
||||
cacheDir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
override val resolver: PackageResolver = PackageResolvers.DiskCachedPackageResolver(SecurityManagers.defaultManager, cacheDir)
|
||||
override val resolver: PackageResolver = PackageResolvers.DiskCachedPackageResolver(
|
||||
SecurityManagers.defaultManager, httpClient, cacheDir)
|
||||
}
|
||||
|
||||
class InMemoryPackageResolverTest : AbstractPackageResolverTest() {
|
||||
override val resolver: PackageResolver = PackageResolvers.InMemoryPackageResolver(SecurityManagers.defaultManager)
|
||||
override val resolver: PackageResolver = PackageResolvers.InMemoryPackageResolver(
|
||||
SecurityManagers.defaultManager, httpClient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.pkl.commons.test.FileTestUtils
|
||||
import org.pkl.commons.test.PackageServer
|
||||
import org.pkl.core.http.HttpClient
|
||||
import org.pkl.core.PklException
|
||||
import org.pkl.core.SecurityManagers
|
||||
import org.pkl.core.packages.PackageResolver
|
||||
import org.pkl.core.runtime.CertificateUtils
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Path
|
||||
@@ -19,16 +19,19 @@ class ProjectDependenciesResolverTest {
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun beforeAll() {
|
||||
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
|
||||
PackageServer.ensureStarted()
|
||||
}
|
||||
|
||||
val httpClient: HttpClient = HttpClient.builder()
|
||||
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveDependencies() {
|
||||
val project2Path = Path.of(javaClass.getResource("project2/PklProject")!!.path)
|
||||
val project = Project.loadFromPath(project2Path)
|
||||
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, null)
|
||||
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null)
|
||||
val deps = ProjectDependenciesResolver(project, packageResolver, System.out.writer()).resolve()
|
||||
val strDeps = ByteArrayOutputStream()
|
||||
.apply { deps.writeTo(this) }
|
||||
@@ -66,7 +69,7 @@ class ProjectDependenciesResolverTest {
|
||||
fun `fails if project declares a package with an incorrect checksum`() {
|
||||
val projectPath = Path.of(javaClass.getResource("badProjectChecksum/PklProject")!!.path)
|
||||
val project = Project.loadFromPath(projectPath)
|
||||
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, null)
|
||||
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null)
|
||||
val e = assertThrows<PklException> {
|
||||
ProjectDependenciesResolver(project, packageResolver, System.err.writer()).resolve()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatCode
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.pkl.commons.test.FileTestUtils
|
||||
import org.pkl.core.http.HttpClient
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import java.util.regex.Pattern
|
||||
@@ -137,9 +139,13 @@ class ProjectTest {
|
||||
PackageServer.ensureStarted()
|
||||
val projectDir = Path.of(javaClass.getResource("badProjectChecksum2/")!!.path)
|
||||
val project = Project.loadFromPath(projectDir.resolve("PklProject"))
|
||||
val httpClient = HttpClient.builder()
|
||||
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||
.build()
|
||||
val evaluator = EvaluatorBuilder.preconfigured()
|
||||
.applyFromProject(project)
|
||||
.setModuleCacheDir(null)
|
||||
.setHttpClient(httpClient)
|
||||
.build()
|
||||
assertThatCode { evaluator.evaluate(ModuleSource.path(projectDir.resolve("bug.pkl"))) }
|
||||
.hasMessageStartingWith("""
|
||||
|
||||
48
pkl-core/src/test/kotlin/org/pkl/core/util/ExceptionsTest.kt
Normal file
48
pkl-core/src/test/kotlin/org/pkl/core/util/ExceptionsTest.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
package org.pkl.core.util
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
import java.lang.Error
|
||||
|
||||
class ExceptionsTest {
|
||||
@Test
|
||||
fun `get root cause of simple exception`() {
|
||||
val e = IOException("io")
|
||||
assertThat(Exceptions.getRootCause(e)).isSameAs(e)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get root cause of nested exception`() {
|
||||
val e = IOException("io")
|
||||
val e2 = RuntimeException("runtime")
|
||||
val e3 = Error("error")
|
||||
e.initCause(e2)
|
||||
e2.initCause(e3)
|
||||
assertThat(Exceptions.getRootCause(e)).isSameAs(e3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get root reason`() {
|
||||
val e = IOException("io")
|
||||
val e2 = RuntimeException("the root reason")
|
||||
e.initCause(e2)
|
||||
assertThat(Exceptions.getRootReason(e)).isEqualTo("the root reason")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get root reason if null`() {
|
||||
val e = IOException("io")
|
||||
val e2 = RuntimeException(null as String?)
|
||||
e.initCause(e2)
|
||||
assertThat(Exceptions.getRootReason(e)).isEqualTo("(unknown reason)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get root reason if empty`() {
|
||||
val e = IOException("io")
|
||||
val e2 = RuntimeException("")
|
||||
e.initCause(e2)
|
||||
assertThat(Exceptions.getRootReason(e)).isEqualTo("(unknown reason)")
|
||||
}
|
||||
}
|
||||
42
pkl-core/src/test/kotlin/org/pkl/core/util/HttpUtilsTest.kt
Normal file
42
pkl-core/src/test/kotlin/org/pkl/core/util/HttpUtilsTest.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package org.pkl.core.util
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.pkl.commons.test.FakeHttpResponse
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
|
||||
class HttpUtilsTest {
|
||||
@Test
|
||||
fun isHttpUrl() {
|
||||
assertThat(HttpUtils.isHttpUrl(URI("http://example.com"))).isTrue
|
||||
assertThat(HttpUtils.isHttpUrl(URI("https://example.com"))).isTrue
|
||||
assertThat(HttpUtils.isHttpUrl(URI("HtTpS://example.com"))).isTrue
|
||||
assertThat(HttpUtils.isHttpUrl(URI("file://example.com"))).isFalse
|
||||
|
||||
assertThat(HttpUtils.isHttpUrl(URL("http://example.com"))).isTrue
|
||||
assertThat(HttpUtils.isHttpUrl(URL("https://example.com"))).isTrue
|
||||
assertThat(HttpUtils.isHttpUrl(URL("HtTpS://example.com"))).isTrue
|
||||
assertThat(HttpUtils.isHttpUrl(URL("file://example.com"))).isFalse
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkHasStatusCode200() {
|
||||
val response = FakeHttpResponse.withoutBody {
|
||||
statusCode = 200
|
||||
}
|
||||
assertDoesNotThrow {
|
||||
HttpUtils.checkHasStatusCode200(response)
|
||||
}
|
||||
|
||||
val response2 = FakeHttpResponse.withoutBody {
|
||||
statusCode = 404
|
||||
}
|
||||
assertThrows<IOException> {
|
||||
HttpUtils.checkHasStatusCode200(response2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import org.pkl.core.runtime.ModuleResolver
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createFile
|
||||
|
||||
@@ -410,4 +411,24 @@ class IoUtilsTest {
|
||||
IoUtils.resolve(FakeSecurityManager, key2, URI("...NamedModuleResolversTest.pkl"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readBytes(URL) does not support HTTP URLs`() {
|
||||
assertThrows<IllegalArgumentException> {
|
||||
IoUtils.readBytes(URL("https://example.com"))
|
||||
}
|
||||
assertThrows<IllegalArgumentException> {
|
||||
IoUtils.readBytes(URL("http://example.com"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readString(URL) does not support HTTP URLs`() {
|
||||
assertThrows<IllegalArgumentException> {
|
||||
IoUtils.readString(URL("https://example.com"))
|
||||
}
|
||||
assertThrows<IllegalArgumentException> {
|
||||
IoUtils.readString(URL("http://example.com"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
broken
|
||||
Reference in New Issue
Block a user