mirror of
https://github.com/apple/pkl.git
synced 2026-06-10 07:42:58 +02:00
Improve HTTP redirect following (#1637)
This implements HTTP redirect following ourselves. The goal is: 1. All I/O is checked against `--allowed-resources` and `--allowed-modules`, including HTTP redirects 2. HTTP rewrite rules can affect redirect following 3. HTTP headers can affect redirect following --------- Co-authored-by: Islon Scherer <islonscherer@gmail.com>
This commit is contained in:
@@ -1295,7 +1295,7 @@ result = someLib.x
|
||||
CliBaseOptions(
|
||||
sourceModules = listOf(moduleUri),
|
||||
workingDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
)
|
||||
)
|
||||
val buffer = ByteArrayOutputStream()
|
||||
@@ -1337,7 +1337,7 @@ result = someLib.x
|
||||
CliBaseOptions(
|
||||
sourceModules = listOf(moduleUri),
|
||||
workingDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
settings = settingsFile,
|
||||
)
|
||||
)
|
||||
@@ -1367,7 +1367,7 @@ result = someLib.x
|
||||
workingDir = tempDir,
|
||||
moduleCacheDir = tempDir,
|
||||
noCache = true,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = packageServer.port,
|
||||
)
|
||||
)
|
||||
@@ -1473,7 +1473,7 @@ result = someLib.x
|
||||
sourceModules = listOf(URI("package://localhost:1/birds@0.5.0#/catalog/Ostrich.pkl")),
|
||||
noCache = true,
|
||||
httpProxy = URI(wwRuntimeInfo.httpBaseUrl),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
allowedModules = SecurityManagers.defaultAllowedModules + Pattern.compile("http:"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ class CliPackageDownloaderTest {
|
||||
baseOptions =
|
||||
CliBaseOptions(
|
||||
moduleCacheDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = server.port,
|
||||
),
|
||||
packageUris =
|
||||
@@ -83,7 +83,7 @@ class CliPackageDownloaderTest {
|
||||
baseOptions =
|
||||
CliBaseOptions(
|
||||
workingDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = server.port,
|
||||
),
|
||||
packageUris = listOf(PackageUri("package://localhost:0/birds@0.5.0")),
|
||||
@@ -103,7 +103,7 @@ class CliPackageDownloaderTest {
|
||||
baseOptions =
|
||||
CliBaseOptions(
|
||||
moduleCacheDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = server.port,
|
||||
),
|
||||
packageUris =
|
||||
@@ -124,7 +124,7 @@ class CliPackageDownloaderTest {
|
||||
baseOptions =
|
||||
CliBaseOptions(
|
||||
moduleCacheDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = server.port,
|
||||
),
|
||||
packageUris =
|
||||
@@ -165,7 +165,7 @@ class CliPackageDownloaderTest {
|
||||
baseOptions =
|
||||
CliBaseOptions(
|
||||
moduleCacheDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = server.port,
|
||||
),
|
||||
packageUris = listOf(PackageUri("package://localhost:0/badChecksum@1.0.0")),
|
||||
@@ -184,7 +184,7 @@ class CliPackageDownloaderTest {
|
||||
baseOptions =
|
||||
CliBaseOptions(
|
||||
moduleCacheDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = server.port,
|
||||
),
|
||||
packageUris =
|
||||
@@ -221,7 +221,7 @@ class CliPackageDownloaderTest {
|
||||
baseOptions =
|
||||
CliBaseOptions(
|
||||
moduleCacheDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = server.port,
|
||||
),
|
||||
packageUris = listOf(PackageUri("package://localhost:0/birds@0.5.0")),
|
||||
|
||||
@@ -967,7 +967,7 @@ class CliProjectPackagerTest {
|
||||
CliProjectPackager(
|
||||
CliBaseOptions(
|
||||
workingDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = packageServer.port,
|
||||
),
|
||||
listOf(tempDir.resolve("project")),
|
||||
@@ -1011,7 +1011,7 @@ class CliProjectPackagerTest {
|
||||
CliProjectPackager(
|
||||
CliBaseOptions(
|
||||
workingDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = packageServer.port,
|
||||
),
|
||||
listOf(tempDir.resolve("project")),
|
||||
|
||||
@@ -87,7 +87,7 @@ class CliProjectResolverTest {
|
||||
CliProjectResolver(
|
||||
CliBaseOptions(
|
||||
workingDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = packageServer.port,
|
||||
noCache = true,
|
||||
),
|
||||
@@ -142,7 +142,7 @@ class CliProjectResolverTest {
|
||||
CliProjectResolver(
|
||||
CliBaseOptions(
|
||||
workingDir = tempDir,
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = packageServer.port,
|
||||
noCache = true,
|
||||
),
|
||||
@@ -240,7 +240,7 @@ class CliProjectResolverTest {
|
||||
)
|
||||
CliProjectResolver(
|
||||
CliBaseOptions(
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = packageServer.port,
|
||||
noCache = true,
|
||||
),
|
||||
@@ -322,7 +322,7 @@ class CliProjectResolverTest {
|
||||
val errOut = StringWriter()
|
||||
CliProjectResolver(
|
||||
CliBaseOptions(
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = packageServer.port,
|
||||
noCache = true,
|
||||
),
|
||||
@@ -397,7 +397,7 @@ class CliProjectResolverTest {
|
||||
val errOut = StringWriter()
|
||||
CliProjectResolver(
|
||||
CliBaseOptions(
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = packageServer.port,
|
||||
noCache = true,
|
||||
),
|
||||
@@ -484,7 +484,7 @@ class CliProjectResolverTest {
|
||||
assertThatCode {
|
||||
CliProjectResolver(
|
||||
CliBaseOptions(
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
|
||||
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
|
||||
testPort = packageServer.port,
|
||||
noCache = true,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,12 +31,19 @@ object FileTestUtils {
|
||||
?: workingDir.parent.parent.takeIf { it.resolve("settings.gradle.kts").exists() }
|
||||
?: throw AssertionError("Failed to locate root project directory.")
|
||||
}
|
||||
val selfSignedCertificate: Path by lazy {
|
||||
|
||||
val selfSignedCertificateP12: Path by lazy {
|
||||
rootProjectDir.resolve("pkl-commons-test/build/keystore/localhost.p12")
|
||||
}
|
||||
|
||||
val selfSignedCertificatePem: Path by lazy {
|
||||
rootProjectDir.resolve("pkl-commons-test/build/keystore/localhost.pem")
|
||||
}
|
||||
|
||||
val selfSignedCertificatePassword = "password"
|
||||
|
||||
fun writeCertificateWithMissingLines(dir: Path): Path {
|
||||
val lines = selfSignedCertificate.readLines()
|
||||
val lines = selfSignedCertificatePem.readLines()
|
||||
// drop some lines in the middle
|
||||
return dir.resolve("invalidCerts.pem").writeLines(lines.take(5) + lines.takeLast(5))
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ dependencies {
|
||||
implementation(libs.snakeYaml)
|
||||
|
||||
testImplementation(projects.pklCommonsTest)
|
||||
testImplementation(libs.wiremock)
|
||||
|
||||
add("generatorImplementation", libs.javaPoet)
|
||||
add("generatorImplementation", libs.truffleApi)
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.graalvm.polyglot.Context;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.pkl.core.evaluatorSettings.TraceMode;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.http.HttpClientException;
|
||||
import org.pkl.core.module.ModuleKeyFactory;
|
||||
import org.pkl.core.module.ProjectDependenciesManager;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
@@ -79,10 +79,7 @@ public class Analyzer {
|
||||
context.enter();
|
||||
var vmContext = VmContext.get(null);
|
||||
return VmImportAnalyzer.analyze(sources, vmContext);
|
||||
} catch (SecurityManagerException
|
||||
| IOException
|
||||
| PackageLoadError
|
||||
| HttpClientInitException e) {
|
||||
} catch (SecurityManagerException | IOException | PackageLoadError | HttpClientException e) {
|
||||
throw new PklException(e.getMessage(), e);
|
||||
} catch (PklException err) {
|
||||
throw err;
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,7 +21,7 @@ import com.oracle.truffle.api.source.SourceSection;
|
||||
import java.util.Map;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.ast.ExpressionNode;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.http.HttpClientException;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
@@ -60,7 +60,7 @@ public final class ImportGlobMemberBodyNode extends ExpressionNode {
|
||||
context.getSecurityManager().checkImportModule(currentModule.getUri(), importUri);
|
||||
var moduleToImport = context.getModuleResolver().resolve(importUri, this);
|
||||
return language.loadModule(moduleToImport, this);
|
||||
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
|
||||
} catch (SecurityManagerException | PackageLoadError | HttpClientException e) {
|
||||
throw exceptionBuilder().withCause(e).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,7 +26,7 @@ import java.net.URI;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.ast.member.SharedMemberNode;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.http.HttpClientException;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
@@ -96,7 +96,7 @@ public class ImportGlobNode extends AbstractImportNode {
|
||||
return cachedResult;
|
||||
} catch (IOException e) {
|
||||
throw exceptionBuilder().evalError("ioErrorResolvingGlob", importUri).withCause(e).build();
|
||||
} catch (SecurityManagerException | HttpClientInitException e) {
|
||||
} catch (SecurityManagerException | HttpClientException e) {
|
||||
throw exceptionBuilder().withCause(e).build();
|
||||
} catch (PackageLoadError e) {
|
||||
throw exceptionBuilder().adhocEvalError(e.getMessage()).build();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,7 +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.http.HttpClientException;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
@@ -55,7 +55,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 | HttpClientInitException e) {
|
||||
} catch (SecurityManagerException | PackageLoadError | HttpClientException e) {
|
||||
throw exceptionBuilder().withCause(e).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,7 +27,7 @@ import org.graalvm.collections.EconomicMap;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.ast.member.SharedMemberNode;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.http.HttpClientException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
import org.pkl.core.runtime.VmLanguage;
|
||||
@@ -97,7 +97,7 @@ public abstract class ReadGlobNode extends AbstractReadNode {
|
||||
return cachedResult;
|
||||
} catch (IOException e) {
|
||||
throw exceptionBuilder().evalError("ioErrorResolvingGlob", globPattern).withCause(e).build();
|
||||
} catch (SecurityManagerException | HttpClientInitException | URISyntaxException e) {
|
||||
} catch (SecurityManagerException | HttpClientException | URISyntaxException e) {
|
||||
throw exceptionBuilder().withCause(e).build();
|
||||
} catch (InvalidGlobPatternException e) {
|
||||
throw exceptionBuilder()
|
||||
|
||||
@@ -24,7 +24,10 @@ import java.net.http.HttpResponse.BodyHandler;
|
||||
@ThreadSafe
|
||||
final class DummyHttpClient implements HttpClient {
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler) {
|
||||
public <T> HttpResponse<T> send(
|
||||
HttpRequest request,
|
||||
BodyHandler<T> responseBodyHandler,
|
||||
HttpRequestChecker httpRequestChecker) {
|
||||
throw new AssertionError("Dummy HTTP client cannot send request: " + request);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,14 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandler;
|
||||
import java.net.http.HttpTimeoutException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
|
||||
/**
|
||||
* An HTTP client.
|
||||
@@ -190,7 +192,7 @@ public interface HttpClient extends AutoCloseable {
|
||||
/**
|
||||
* Creates a new {@code HttpClient} from the current state of this builder.
|
||||
*
|
||||
* @throws HttpClientInitException if an error occurs while initializing the client
|
||||
* @throws HttpClientException if an error occurs while initializing the client
|
||||
*/
|
||||
HttpClient build();
|
||||
|
||||
@@ -242,11 +244,14 @@ public interface HttpClient extends AutoCloseable {
|
||||
* 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
|
||||
* @throws HttpClientException if a known (user-presentable) error occurs.
|
||||
* @throws SecurityManagerException based on {@code httpRequestChecker}
|
||||
*/
|
||||
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
|
||||
throws IOException;
|
||||
<T> HttpResponse<T> send(
|
||||
HttpRequest request,
|
||||
BodyHandler<T> responseBodyHandler,
|
||||
HttpRequestChecker httpRequestChecker)
|
||||
throws IOException, SecurityManagerException;
|
||||
|
||||
/**
|
||||
* Closes this client.
|
||||
@@ -258,4 +263,9 @@ public interface HttpClient extends AutoCloseable {
|
||||
* {@link IllegalStateException}.
|
||||
*/
|
||||
void close();
|
||||
|
||||
@FunctionalInterface
|
||||
interface HttpRequestChecker {
|
||||
void check(URI uri) throws SecurityManagerException;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,19 +16,19 @@
|
||||
package org.pkl.core.http;
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred while initializing an HTTP client. A common example is an error
|
||||
* reading or parsing a certificate.
|
||||
* Indicates that an error occurred while initializing an HTTP client, or when making an HTTP call.
|
||||
* The error messages are user-presentable.
|
||||
*/
|
||||
public final class HttpClientInitException extends RuntimeException {
|
||||
public HttpClientInitException(String message) {
|
||||
public final class HttpClientException extends RuntimeException {
|
||||
public HttpClientException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HttpClientInitException(Throwable cause) {
|
||||
public HttpClientException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public HttpClientInitException(String message, Throwable cause) {
|
||||
public HttpClientException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.Exceptions;
|
||||
|
||||
@@ -83,13 +84,16 @@ final class JdkHttpClient implements HttpClient {
|
||||
.sslContext(createSslContext(certificateFiles, certificateBytes))
|
||||
.connectTimeout(connectTimeout)
|
||||
.proxy(proxySelector)
|
||||
.followRedirects(Redirect.NORMAL)
|
||||
.followRedirects(Redirect.NEVER)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||
throws IOException {
|
||||
public <T> HttpResponse<T> send(
|
||||
HttpRequest request,
|
||||
BodyHandler<T> responseBodyHandler,
|
||||
HttpRequestChecker httpRequestChecker)
|
||||
throws IOException, SecurityManagerException {
|
||||
try {
|
||||
return underlying.send(request, responseBodyHandler);
|
||||
} catch (ConnectException e) {
|
||||
@@ -144,7 +148,7 @@ final class JdkHttpClient implements HttpClient {
|
||||
|
||||
return sslContext;
|
||||
} catch (GeneralSecurityException | IOException e) {
|
||||
throw new HttpClientInitException(
|
||||
throw new HttpClientException(
|
||||
ErrorMessages.create("cannotInitHttpClient", Exceptions.getRootReason(e)), e);
|
||||
}
|
||||
}
|
||||
@@ -156,9 +160,9 @@ final class JdkHttpClient implements HttpClient {
|
||||
try (var stream = Files.newInputStream(file)) {
|
||||
collectCertificates(certificates, factory, stream, file);
|
||||
} catch (NoSuchFileException e) {
|
||||
throw new HttpClientInitException(ErrorMessages.create("cannotFindCertFile", file));
|
||||
throw new HttpClientException(ErrorMessages.create("cannotFindCertFile", file));
|
||||
} catch (IOException e) {
|
||||
throw new HttpClientInitException(
|
||||
throw new HttpClientException(
|
||||
ErrorMessages.create("cannotReadCertFile", Exceptions.getRootReason(e)));
|
||||
}
|
||||
}
|
||||
@@ -179,11 +183,11 @@ final class JdkHttpClient implements HttpClient {
|
||||
//noinspection unchecked
|
||||
certificates = (Collection<X509Certificate>) factory.generateCertificates(stream);
|
||||
} catch (CertificateException e) {
|
||||
throw new HttpClientInitException(
|
||||
throw new HttpClientException(
|
||||
ErrorMessages.create("cannotParseCertFile", source, Exceptions.getRootReason(e)));
|
||||
}
|
||||
if (certificates.isEmpty()) {
|
||||
throw new HttpClientInitException(ErrorMessages.create("emptyCertFile", source));
|
||||
throw new HttpClientException(ErrorMessages.create("emptyCertFile", source));
|
||||
}
|
||||
anchors.addAll(certificates);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.net.http.HttpResponse.BodyHandler;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
|
||||
/**
|
||||
* An {@code HttpClient} decorator that defers creating the underlying HTTP client until the first
|
||||
@@ -45,9 +46,12 @@ final class LazyHttpClient implements HttpClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||
throws IOException {
|
||||
return getOrCreateClient().send(request, responseBodyHandler);
|
||||
public <T> HttpResponse<T> send(
|
||||
HttpRequest request,
|
||||
BodyHandler<T> responseBodyHandler,
|
||||
HttpRequestChecker httpRequestChecker)
|
||||
throws IOException, SecurityManagerException {
|
||||
return getOrCreateClient().send(request, responseBodyHandler, httpRequestChecker);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -34,6 +34,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.pkl.core.PklBugException;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.HttpUtils;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
import org.pkl.core.util.Pair;
|
||||
@@ -42,13 +44,15 @@ import org.pkl.core.util.Pair;
|
||||
* An {@code HttpClient} decorator that
|
||||
*
|
||||
* <ul>
|
||||
* <li>overrides the {@code User-Agent} header of {@code HttpRequest}s
|
||||
* <li>overrides the headers of {@code HttpRequest}s
|
||||
* <li>sets a request timeout if none is present
|
||||
* <li>ensures that {@link #close()} is idempotent.
|
||||
* <li>rewrites outbound URI prefixes with another prefix.
|
||||
* <li>handles redirects, rewriting URLs after each redirect and checking against {@link
|
||||
* org.pkl.core.http.HttpClient.HttpRequestChecker}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Both {@code User-Agent} header and default request timeout are configurable through {@link
|
||||
* <p>The headers, default request timeout, and URI rewrites are configurable through {@link
|
||||
* HttpClient.Builder}.
|
||||
*/
|
||||
@ThreadSafe
|
||||
@@ -64,6 +68,22 @@ final class RequestRewritingClient implements HttpClient {
|
||||
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
private static final int MAX_HTTP_REDIRECTS;
|
||||
|
||||
static {
|
||||
// allow Java users to configure max redirects the same way they would Java's default HTTP
|
||||
// client.
|
||||
var maxRedirectProp = System.getProperty("http.maxRedirects");
|
||||
var maxRedirects = 20;
|
||||
if (maxRedirectProp != null) {
|
||||
try {
|
||||
maxRedirects = Math.max(0, Integer.parseInt(maxRedirectProp));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
MAX_HTTP_REDIRECTS = maxRedirects;
|
||||
}
|
||||
|
||||
RequestRewritingClient(
|
||||
String userAgent,
|
||||
Duration requestTimeout,
|
||||
@@ -84,12 +104,56 @@ final class RequestRewritingClient implements HttpClient {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
private <T> HttpResponse<T> doSend(
|
||||
HttpRequest request,
|
||||
BodyHandler<T> responseBodyHandler,
|
||||
HttpRequestChecker httpRequestChecker)
|
||||
throws SecurityManagerException, IOException {
|
||||
var redirectCount = 0;
|
||||
var currentRequestUri = rewriteUri(request.uri());
|
||||
var currentRequest = rewriteRequest(request, currentRequestUri);
|
||||
while (true) {
|
||||
httpRequestChecker.check(currentRequestUri);
|
||||
var response = delegate.send(currentRequest, responseBodyHandler, httpRequestChecker);
|
||||
if (!HttpUtils.isRedirectStatusCode(response.statusCode())) {
|
||||
return response;
|
||||
}
|
||||
if (redirectCount >= MAX_HTTP_REDIRECTS) {
|
||||
throw new HttpClientException(
|
||||
ErrorMessages.create("httpTooManyRedirects", MAX_HTTP_REDIRECTS));
|
||||
}
|
||||
var location = response.headers().firstValue("Location");
|
||||
if (location.isEmpty()) {
|
||||
throw new HttpClientException(
|
||||
ErrorMessages.create("httpRedirectNoLocation", currentRequestUri));
|
||||
}
|
||||
URI redirectUri;
|
||||
try {
|
||||
redirectUri = currentRequestUri.resolve(location.get());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new HttpClientException(
|
||||
ErrorMessages.create("httpRedirectInvalidUri", currentRequestUri, location.get()));
|
||||
}
|
||||
if (currentRequestUri.getScheme().equalsIgnoreCase("https")
|
||||
&& redirectUri.getScheme().equalsIgnoreCase("http")) {
|
||||
throw new HttpClientException(
|
||||
ErrorMessages.create("httpRedirectCannotDowngrade", currentRequestUri, redirectUri));
|
||||
}
|
||||
currentRequestUri = rewriteUri(redirectUri);
|
||||
currentRequest = rewriteRequest(request, currentRequestUri);
|
||||
redirectCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
|
||||
throws IOException {
|
||||
public <T> HttpResponse<T> send(
|
||||
HttpRequest request,
|
||||
BodyHandler<T> responseBodyHandler,
|
||||
HttpRequestChecker httpRequestChecker)
|
||||
throws IOException, SecurityManagerException {
|
||||
checkNotClosed(request);
|
||||
try {
|
||||
return delegate.send(rewriteRequest(request), responseBodyHandler);
|
||||
return doSend(request, responseBodyHandler, httpRequestChecker);
|
||||
} catch (IOException e) {
|
||||
var rewrittenUri = rewriteUri(request.uri());
|
||||
if (rewrittenUri != request.uri()) {
|
||||
@@ -108,11 +172,11 @@ final class RequestRewritingClient implements HttpClient {
|
||||
}
|
||||
|
||||
// Based on JDK 17's implementation of HttpRequest.newBuilder(HttpRequest, filter).
|
||||
private HttpRequest rewriteRequest(HttpRequest original) {
|
||||
private HttpRequest rewriteRequest(HttpRequest original, URI newUri) {
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder();
|
||||
|
||||
builder
|
||||
.uri(rewriteUri(original.uri()))
|
||||
.uri(newUri)
|
||||
.expectContinue(original.expectContinue())
|
||||
.timeout(original.timeout().orElse(requestTimeout))
|
||||
.version(original.version().orElse(java.net.http.HttpClient.Version.HTTP_2));
|
||||
@@ -122,7 +186,7 @@ final class RequestRewritingClient implements HttpClient {
|
||||
.map()
|
||||
.forEach((name, values) -> values.forEach(value -> builder.header(name, value)));
|
||||
var isUserAgentSet = false;
|
||||
for (var header : this.getHeaders(original.uri())) {
|
||||
for (var header : this.getHeaders(newUri)) {
|
||||
var headerName = header.getFirst();
|
||||
isUserAgentSet = isUserAgentSet || headerName.equalsIgnoreCase("user-agent");
|
||||
builder.header(header.getFirst(), header.getSecond());
|
||||
|
||||
@@ -519,13 +519,13 @@ public final class ModuleKeys {
|
||||
@Override
|
||||
public ResolvedModuleKey resolve(SecurityManager securityManager)
|
||||
throws IOException, SecurityManagerException {
|
||||
securityManager.checkResolveModule(uri);
|
||||
var httpClient = VmContext.get(null).getHttpClient();
|
||||
var request = HttpRequest.newBuilder(uri).build();
|
||||
var response = httpClient.send(request, BodyHandlers.ofInputStream());
|
||||
var response =
|
||||
httpClient.send(
|
||||
request, BodyHandlers.ofInputStream(), securityManager::checkResolveModule);
|
||||
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);
|
||||
|
||||
@@ -201,7 +201,9 @@ final class PackageResolvers {
|
||||
var request = HttpRequest.newBuilder(uri).build();
|
||||
HttpResponse<InputStream> response;
|
||||
try {
|
||||
response = httpClient.send(request, BodyHandlers.ofInputStream());
|
||||
response =
|
||||
httpClient.send(
|
||||
request, BodyHandlers.ofInputStream(), securityManager::checkReadResource);
|
||||
} catch (IOException e) {
|
||||
throw new PackageLoadError(e, "ioErrorMakingHttpGet", uri, e.getMessage());
|
||||
}
|
||||
|
||||
@@ -257,7 +257,8 @@ public final class ResourceReaders {
|
||||
static final ResourceReader INSTANCE = new FileResource();
|
||||
|
||||
@Override
|
||||
public Optional<Object> read(URI uri) throws IOException, URISyntaxException {
|
||||
public Optional<Object> read(URI uri)
|
||||
throws IOException, URISyntaxException, SecurityManagerException {
|
||||
IoUtils.validateFileUri(uri);
|
||||
// Use resolveSecurePath to get a symlink-free path verified under rootDir.
|
||||
var securityManager = VmContext.get(null).getSecurityManager();
|
||||
@@ -307,7 +308,7 @@ public final class ResourceReaders {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HttpResource extends UrlResource {
|
||||
public static final class HttpResource extends UrlResource {
|
||||
static final ResourceReader INSTANCE = new HttpResource();
|
||||
|
||||
@Override
|
||||
@@ -326,7 +327,7 @@ public final class ResourceReaders {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HttpsResource extends UrlResource {
|
||||
public static final class HttpsResource extends UrlResource {
|
||||
static final ResourceReader INSTANCE = new HttpsResource();
|
||||
|
||||
@Override
|
||||
@@ -347,11 +348,16 @@ public final class ResourceReaders {
|
||||
|
||||
private abstract static class UrlResource implements ResourceReader {
|
||||
@Override
|
||||
public Optional<Object> read(URI uri) throws IOException, URISyntaxException {
|
||||
public Optional<Object> read(URI uri)
|
||||
throws IOException, URISyntaxException, SecurityManagerException {
|
||||
if (HttpUtils.isHttpUrl(uri)) {
|
||||
var httpClient = VmContext.get(null).getHttpClient();
|
||||
var vmContext = VmContext.get(null);
|
||||
var securityManager = vmContext.getSecurityManager();
|
||||
var httpClient = vmContext.getHttpClient();
|
||||
var request = HttpRequest.newBuilder(uri).build();
|
||||
var response = httpClient.send(request, BodyHandlers.ofByteArray());
|
||||
var response =
|
||||
httpClient.send(
|
||||
request, BodyHandlers.ofByteArray(), securityManager::checkReadResource);
|
||||
if (response.statusCode() == 404) return Optional.empty();
|
||||
HttpUtils.checkHasStatusCode200(response);
|
||||
return Optional.of(new Resource(uri, response.body()));
|
||||
|
||||
@@ -31,7 +31,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.pkl.core.Release;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.http.HttpClientException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.module.ModuleKeys;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
@@ -198,7 +198,7 @@ public final class ModuleCache {
|
||||
ModuleKey module, SecurityManager securityManager, @Nullable Node importNode) {
|
||||
try {
|
||||
return module.resolve(securityManager);
|
||||
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
|
||||
} catch (SecurityManagerException | PackageLoadError | HttpClientException e) {
|
||||
throw new VmExceptionBuilder().withOptionalLocation(importNode).withCause(e).build();
|
||||
} catch (FileNotFoundException | NoSuchFileException e) {
|
||||
var exceptionBuilder =
|
||||
|
||||
@@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.http.HttpClientException;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.resource.Resource;
|
||||
import org.pkl.core.resource.ResourceReader;
|
||||
import org.pkl.core.resource.ResourceReaders;
|
||||
import org.pkl.core.stdlib.VmObjectFactory;
|
||||
|
||||
public final class ResourceManager {
|
||||
@@ -86,7 +87,7 @@ public final class ResourceManager {
|
||||
.build();
|
||||
} catch (SecurityManagerException
|
||||
| PackageLoadError
|
||||
| HttpClientInitException
|
||||
| HttpClientException
|
||||
| ExternalReaderProcessException e) {
|
||||
throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build();
|
||||
}
|
||||
@@ -98,12 +99,17 @@ public final class ResourceManager {
|
||||
return resources.computeIfAbsent(
|
||||
resourceUri.normalize(),
|
||||
(uri) -> {
|
||||
try {
|
||||
securityManager.checkReadResource(uri);
|
||||
} catch (SecurityManagerException e) {
|
||||
throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build();
|
||||
}
|
||||
var reader = getResourceReader(uri);
|
||||
// hack: we don't want to call `checkReadResource` here for these resources because those
|
||||
// readers defer to HttpClient to do the actual checks.
|
||||
if (!(reader instanceof ResourceReaders.HttpResource)
|
||||
&& !(reader instanceof ResourceReaders.HttpsResource)) {
|
||||
try {
|
||||
securityManager.checkReadResource(uri);
|
||||
} catch (SecurityManagerException e) {
|
||||
throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build();
|
||||
}
|
||||
}
|
||||
if (reader == null) {
|
||||
throw new VmExceptionBuilder()
|
||||
.withOptionalLocation(readNode)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +25,17 @@ import org.pkl.core.PklBugException;
|
||||
public final class HttpUtils {
|
||||
private HttpUtils() {}
|
||||
|
||||
public static boolean isRedirectStatusCode(int statusCode) {
|
||||
return switch (statusCode) {
|
||||
// We can handle each of these status codes exactly the same because:
|
||||
//
|
||||
// 1. We don't implement any caching for HTTPS requests.
|
||||
// 2. Pkl only makes GET requests.
|
||||
case 301, 302, 303, 307, 308 -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isHttpUrl(URL url) {
|
||||
var protocol = url.getProtocol();
|
||||
return "https".equalsIgnoreCase(protocol) || "http".equalsIgnoreCase(protocol);
|
||||
|
||||
@@ -1151,3 +1151,23 @@ HTTP header value `{0}` has invalid syntax.
|
||||
invalidHttpHeaderValueTooLong=\
|
||||
HTTP Header value is invalid because it is longer than 4096 characters. \
|
||||
Value: `{0}`
|
||||
|
||||
httpTooManyRedirects=\
|
||||
Too many redirects, exceeded the redirect threshold ({0}).
|
||||
|
||||
httpRedirectNoLocation=\
|
||||
Cannot follow HTTP redirect because no ''Location'' header was found.\n\
|
||||
\n\
|
||||
HTTP Request: `GET {0}`
|
||||
|
||||
httpRedirectInvalidUri=\
|
||||
Cannot follow HTTP redirect because the response ''Location'' header has a malformed URI.\n\
|
||||
\n\
|
||||
HTTP Request: `GET {0}`\n\
|
||||
Location header: `{1}`
|
||||
|
||||
httpRedirectCannotDowngrade=\
|
||||
Cannot follow redirect from ''https:'' URL to ''http:'' URL.\
|
||||
\n\
|
||||
HTTP Request: `GET {0}`\n\
|
||||
Redirected to: `{1}`
|
||||
|
||||
@@ -175,7 +175,7 @@ class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() {
|
||||
.setHttpClient(
|
||||
HttpClient.builder()
|
||||
.setTestPort(packageServer.port)
|
||||
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||
.addCertificates(FileTestUtils.selfSignedCertificatePem)
|
||||
.buildLazily()
|
||||
)
|
||||
.setPowerAssertionsEnabled(true)
|
||||
@@ -287,7 +287,7 @@ abstract class AbstractNativeLanguageSnippetTestsEngine : AbstractLanguageSnippe
|
||||
add("--settings")
|
||||
add("pkl:settings")
|
||||
add("--ca-certificates")
|
||||
add(FileTestUtils.selfSignedCertificate.toString())
|
||||
add(FileTestUtils.selfSignedCertificatePem.toString())
|
||||
add("--test-mode")
|
||||
add("--test-port")
|
||||
add(packageServer.port.toString())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,9 +28,13 @@ class DummyHttpClientTest {
|
||||
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(), NoopChecker)
|
||||
}
|
||||
|
||||
assertThrows<AssertionError> { client.send(request, HttpResponse.BodyHandlers.discarding()) }
|
||||
assertThrows<AssertionError> {
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding(), NoopChecker)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,17 +15,31 @@
|
||||
*/
|
||||
package org.pkl.core.http
|
||||
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.get
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.matching
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.ok
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.permanentRedirect
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.stubFor
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
|
||||
import com.github.tomakehurst.wiremock.client.WireMock.verify
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockExtension
|
||||
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.absolutePathString
|
||||
import kotlin.io.path.createFile
|
||||
import kotlin.io.path.readBytes
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatCode
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.pkl.commons.test.FileTestUtils
|
||||
import org.pkl.core.Release
|
||||
@@ -68,14 +82,16 @@ class HttpClientTest {
|
||||
@Test
|
||||
fun `can load certificates from regular file`() {
|
||||
assertDoesNotThrow {
|
||||
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate).build()
|
||||
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificatePem).build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can load certificates from a byte array`() {
|
||||
assertDoesNotThrow {
|
||||
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate.readBytes()).build()
|
||||
HttpClient.builder()
|
||||
.addCertificates(FileTestUtils.selfSignedCertificatePem.readBytes())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +99,7 @@ class HttpClientTest {
|
||||
fun `certificate file cannot be empty`(@TempDir tempDir: Path) {
|
||||
val file = tempDir.resolve("certs.pem").createFile()
|
||||
|
||||
val e =
|
||||
assertThrows<HttpClientInitException> { HttpClient.builder().addCertificates(file).build() }
|
||||
val e = assertThrows<HttpClientException> { HttpClient.builder().addCertificates(file).build() }
|
||||
|
||||
assertThat(e).hasMessageContaining("empty")
|
||||
}
|
||||
@@ -112,10 +127,185 @@ class HttpClientTest {
|
||||
client.close()
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding(), NoopChecker)
|
||||
}
|
||||
assertThrows<IllegalStateException> {
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding())
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding(), NoopChecker)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class RedirectsTest {
|
||||
// incorrect diagnostic
|
||||
@Suppress("JUnitMalformedDeclaration")
|
||||
@RegisterExtension
|
||||
val wireMock: WireMockExtension =
|
||||
with(WireMockExtension.newInstance()) {
|
||||
configureStaticDsl(true)
|
||||
options(
|
||||
wireMockConfig().apply {
|
||||
dynamicPort()
|
||||
dynamicHttpsPort()
|
||||
keystorePath(FileTestUtils.selfSignedCertificateP12.absolutePathString())
|
||||
keystorePassword(FileTestUtils.selfSignedCertificatePassword)
|
||||
keystoreType("PKCS12")
|
||||
}
|
||||
)
|
||||
build()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `follows redirects`() {
|
||||
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
|
||||
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(ok("bar = 1")))
|
||||
val client = HttpClient.builder().build()
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker)
|
||||
assert(response.body() == "bar = 1")
|
||||
verify(getRequestedFor(urlEqualTo("/foo.pkl")))
|
||||
verify(getRequestedFor(urlEqualTo("/bar.pkl")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `preserves configured headers across redirects`() {
|
||||
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
|
||||
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(ok("bar = 1")))
|
||||
|
||||
val client =
|
||||
HttpClient.builder().addHeaders("**", mapOf("x-foo" to listOf("foo value"))).build()
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker)
|
||||
assert(response.body() == "bar = 1")
|
||||
verify(getRequestedFor(urlEqualTo("/foo.pkl")).withHeader("x-foo", matching("foo value")))
|
||||
verify(getRequestedFor(urlEqualTo("/bar.pkl")).withHeader("x-foo", matching("foo value")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `respects configured rewrites across redirects`() {
|
||||
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/orig/bar.pkl")))
|
||||
stubFor(get(urlEqualTo("/rewritten/bar.pkl")).willReturn(ok()))
|
||||
|
||||
val client =
|
||||
HttpClient.builder()
|
||||
.addRewrite(
|
||||
URI("${wireMock.runtimeInfo.httpBaseUrl}/orig/"),
|
||||
URI("${wireMock.runtimeInfo.httpBaseUrl}/rewritten/"),
|
||||
)
|
||||
.build()
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
|
||||
client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker)
|
||||
verify(getRequestedFor(urlEqualTo("/foo.pkl")))
|
||||
verify(getRequestedFor(urlEqualTo("/rewritten/bar.pkl")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot downgrade HTTPS to HTTP`() {
|
||||
stubFor(
|
||||
get(urlEqualTo("/foo.pkl"))
|
||||
.willReturn(permanentRedirect("${wireMock.runtimeInfo.httpBaseUrl}/bar.pkl"))
|
||||
)
|
||||
|
||||
val client =
|
||||
HttpClient.builder()
|
||||
.addCertificates(FileTestUtils.selfSignedCertificatePem)
|
||||
.addHeaders("**", mapOf("x-foo" to listOf("foo value")))
|
||||
.build()
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpsBaseUrl}/foo.pkl")).build()
|
||||
assertThatCode { client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker) }
|
||||
.hasMessageContaining("Cannot follow redirect from 'https:' URL to 'http:' URL")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can upgrade HTTP to HTTPS`() {
|
||||
stubFor(
|
||||
get(urlEqualTo("/foo.pkl"))
|
||||
.willReturn(permanentRedirect("${wireMock.runtimeInfo.httpsBaseUrl}/bar.pkl"))
|
||||
)
|
||||
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(ok("hello")))
|
||||
|
||||
val client =
|
||||
HttpClient.builder()
|
||||
.addCertificates(FileTestUtils.selfSignedCertificatePem)
|
||||
.addHeaders("**", mapOf("x-foo" to listOf("foo value")))
|
||||
.build()
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker)
|
||||
assertThat(response.body()).isEqualTo("hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `infinite redirects fail with VmException`() {
|
||||
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
|
||||
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(permanentRedirect("/foo.pkl")))
|
||||
val client = HttpClient.builder().build()
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
|
||||
assertThatCode { client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker) }
|
||||
.hasMessageContaining("Too many redirects")
|
||||
verify(getRequestedFor(urlEqualTo("/foo.pkl")))
|
||||
verify(getRequestedFor(urlEqualTo("/bar.pkl")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid redirect URI fails with VmException`() {
|
||||
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("http://not a valid url/")))
|
||||
val client = HttpClient.builder().build()
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
|
||||
assertThatCode { client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker) }
|
||||
.hasMessageContaining(
|
||||
"""
|
||||
Cannot follow HTTP redirect because the response Location header has a malformed URI.
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
verify(getRequestedFor(urlEqualTo("/foo.pkl")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `checks each URL before making a request`() {
|
||||
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
|
||||
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(permanentRedirect("/qux.pkl")))
|
||||
stubFor(get(urlEqualTo("/qux.pkl")).willReturn(ok()))
|
||||
val checkedUrls = mutableListOf<URI>()
|
||||
val checker = HttpClient.HttpRequestChecker { uri -> checkedUrls.add(uri) }
|
||||
val client = HttpClient.builder().build()
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
|
||||
client.send(request, HttpResponse.BodyHandlers.ofString(), checker)
|
||||
assertThat(checkedUrls).hasSize(3)
|
||||
assertThat(checkedUrls)
|
||||
.usingRecursiveComparison()
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl"),
|
||||
URI("${wireMock.runtimeInfo.httpBaseUrl}/bar.pkl"),
|
||||
URI("${wireMock.runtimeInfo.httpBaseUrl}/qux.pkl"),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redirects only carry their specifically configured headers`() {
|
||||
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
|
||||
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(ok()))
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
|
||||
val client =
|
||||
with(HttpClient.builder()) {
|
||||
addHeaders("**/foo.pkl", mapOf("x-foo" to listOf("foo value")))
|
||||
addHeaders("**/bar.pkl", mapOf("x-bar" to listOf("bar value")))
|
||||
build()
|
||||
}
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding(), NoopChecker)
|
||||
verify(getRequestedFor(urlEqualTo("/foo.pkl")).withHeader("x-foo", matching("foo value")))
|
||||
verify(getRequestedFor(urlEqualTo("/bar.pkl")).withoutHeader("x-foo"))
|
||||
verify(getRequestedFor(urlEqualTo("/bar.pkl")).withHeader("x-bar", matching("bar value")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -33,7 +33,9 @@ class LazyHttpClientTest {
|
||||
val client = HttpClient.builder().addCertificates(certFile).buildLazily()
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
|
||||
|
||||
assertThrows<HttpClientInitException> { client.send(request, BodyHandlers.discarding()) }
|
||||
assertThrows<HttpClientException> {
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +25,7 @@ class RequestCapturingClient : HttpClient {
|
||||
override fun <T : Any> send(
|
||||
request: HttpRequest,
|
||||
responseBodyHandler: HttpResponse.BodyHandler<T>,
|
||||
httpRequestChecker: HttpClient.HttpRequestChecker,
|
||||
): HttpResponse<T> {
|
||||
this.request = request
|
||||
return FakeHttpResponse()
|
||||
|
||||
@@ -45,7 +45,7 @@ class RequestRewritingClientTest {
|
||||
|
||||
@Test
|
||||
fun `fills in missing User-Agent header`() {
|
||||
client.send(exampleRequest, BodyHandlers.discarding())
|
||||
client.send(exampleRequest, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("Pkl")
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class RequestRewritingClientTest {
|
||||
mapOf(URI("https://foo/") to URI("https://bar/")),
|
||||
mapOf(IoUtils.doubleStarGlob to mapOf("User-Agent" to listOf("My-User-Agent"))),
|
||||
)
|
||||
client.send(exampleRequest, BodyHandlers.discarding())
|
||||
client.send(exampleRequest, BodyHandlers.discarding(), NoopChecker)
|
||||
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("My-User-Agent")
|
||||
}
|
||||
|
||||
@@ -73,14 +73,14 @@ class RequestRewritingClientTest {
|
||||
.header("User-Agent", "Agent 2")
|
||||
.build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("Pkl")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fills in missing request timeout`() {
|
||||
client.send(exampleRequest, BodyHandlers.discarding())
|
||||
client.send(exampleRequest, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.timeout()).hasValue(Duration.ofSeconds(42))
|
||||
}
|
||||
@@ -89,14 +89,14 @@ class RequestRewritingClientTest {
|
||||
fun `leaves existing request timeout intact`() {
|
||||
val request = HttpRequest.newBuilder(exampleUri).timeout(Duration.ofMinutes(33)).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.timeout()).hasValue(Duration.ofMinutes(33))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fills in missing HTTP version`() {
|
||||
client.send(exampleRequest, BodyHandlers.discarding())
|
||||
client.send(exampleRequest, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.version()).hasValue(JdkHttpClient.Version.HTTP_2)
|
||||
}
|
||||
@@ -105,7 +105,7 @@ class RequestRewritingClientTest {
|
||||
fun `leaves existing HTTP version intact`() {
|
||||
val request = HttpRequest.newBuilder(exampleUri).version(JdkHttpClient.Version.HTTP_1_1).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.version()).hasValue(JdkHttpClient.Version.HTTP_1_1)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ class RequestRewritingClientTest {
|
||||
fun `leaves default method intact`() {
|
||||
val request = HttpRequest.newBuilder(exampleUri).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.method()).isEqualTo("GET")
|
||||
}
|
||||
@@ -123,7 +123,7 @@ class RequestRewritingClientTest {
|
||||
fun `leaves explicit method intact`() {
|
||||
val request = HttpRequest.newBuilder(exampleUri).DELETE().build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.method()).isEqualTo("DELETE")
|
||||
}
|
||||
@@ -133,7 +133,7 @@ class RequestRewritingClientTest {
|
||||
val publisher = BodyPublishers.ofString("body")
|
||||
val request = HttpRequest.newBuilder(exampleUri).PUT(publisher).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.bodyPublisher().get()).isSameAs(publisher)
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class RequestRewritingClientTest {
|
||||
RequestRewritingClient("Pkl", Duration.ofSeconds(42), 5000, captured, mapOf(), mapOf())
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com:0")).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.uri().port).isEqualTo(5000)
|
||||
}
|
||||
@@ -154,7 +154,7 @@ class RequestRewritingClientTest {
|
||||
fun `leaves port 0 intact if no test port is set`() {
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com:0")).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.uri().port).isEqualTo(0)
|
||||
}
|
||||
@@ -344,7 +344,7 @@ class RequestRewritingClientTest {
|
||||
val captured = RequestCapturingClient()
|
||||
val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), -1, captured, rules, mapOf())
|
||||
val request = HttpRequest.newBuilder(URI(uri)).build()
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
return captured.request.uri().toString()
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ class RequestRewritingClientTest {
|
||||
)
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com/foo/bar")).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThatList(captured.request.headers().allValues("x-one")).containsExactly("one")
|
||||
assertThatList(captured.request.headers().allValues("x-two")).containsExactly("two-a", "two-b")
|
||||
@@ -389,7 +389,7 @@ class RequestRewritingClientTest {
|
||||
)
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com/foo/bar")).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThat(captured.request.headers().firstValue("x-foo")).isEmpty
|
||||
assertThat(captured.request.headers().firstValue("x-bar")).isEmpty
|
||||
@@ -413,7 +413,7 @@ class RequestRewritingClientTest {
|
||||
val request =
|
||||
HttpRequest.newBuilder(URI("https://example.com/foo/bar")).header("x-foo", "request").build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThatList(captured.request.headers().allValues("x-foo"))
|
||||
.containsExactly("request", "rule-a", "rule-b")
|
||||
@@ -436,7 +436,7 @@ class RequestRewritingClientTest {
|
||||
)
|
||||
val request = HttpRequest.newBuilder(URI("https://example.com/foo/bar")).build()
|
||||
|
||||
client.send(request, BodyHandlers.discarding())
|
||||
client.send(request, BodyHandlers.discarding(), NoopChecker)
|
||||
|
||||
assertThatList(captured.request.headers().allValues("user-agent"))
|
||||
.containsExactly("My User Agent")
|
||||
|
||||
@@ -28,3 +28,7 @@ fun HttpClient.getConfiguredSettings(): HttpSettings {
|
||||
val requestRewritingClient = this.orCreateClient as RequestRewritingClient
|
||||
return HttpSettings(requestRewritingClient.headers, requestRewritingClient.rewritesMap)
|
||||
}
|
||||
|
||||
object NoopChecker : HttpClient.HttpRequestChecker {
|
||||
override fun check(uri: URI) {}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class PackageResolversTest {
|
||||
|
||||
val httpClient: HttpClient by lazy {
|
||||
HttpClient.builder()
|
||||
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||
.addCertificates(FileTestUtils.selfSignedCertificatePem)
|
||||
.setTestPort(packageServer.port)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -41,7 +41,7 @@ class ProjectDependenciesResolverTest {
|
||||
|
||||
val httpClient: HttpClient by lazy {
|
||||
HttpClient.builder()
|
||||
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||
.addCertificates(FileTestUtils.selfSignedCertificatePem)
|
||||
.setTestPort(packageServer.port)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ class ProjectTest {
|
||||
val project = Project.loadFromPath(projectDir.resolve("PklProject"))
|
||||
val httpClient =
|
||||
HttpClient.builder()
|
||||
.addCertificates(FileTestUtils.selfSignedCertificate)
|
||||
.addCertificates(FileTestUtils.selfSignedCertificatePem)
|
||||
.setTestPort(server.port)
|
||||
.build()
|
||||
val evaluator =
|
||||
|
||||
@@ -557,7 +557,7 @@ class EmbeddedExecutorTest {
|
||||
allowedModules("file:", "package:", "https:")
|
||||
allowedResources("prop:", "package:", "https:")
|
||||
moduleCacheDir(cacheDir)
|
||||
certificateFiles(FileTestUtils.selfSignedCertificate)
|
||||
certificateFiles(FileTestUtils.selfSignedCertificatePem)
|
||||
testPort(server.port)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ class Http {
|
||||
/// In the following example, an original request for `https://pkg.pkl-lang.org/my/pkg@1.0.0` is
|
||||
/// replaced with `https://my.internal.mirror/my/pkg@1.0.0`.
|
||||
///
|
||||
/// This does not affect `3XX` status code redirect following.
|
||||
/// This also affects `3XX` status code redirect following.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user