Files
pkl/stdlib/EvaluatorSettings.pkl
T
Daniel Chao 8e2e5e4ba8 Improve HTTP headers logic (#1584)
* Relax forbidden headers constraints
  - remove restriction on browser-related headers
- allow any glob pattern (no need to end with `/` or `*`, because glob
patterns already require users to explicitly declare prefix matches if
that's the intention)
* Replace `List<Pair<, ...>>`; use `Map<String, ...>` instead
* Use glob pattern strings as an API throughout, instead of `Pattern`
(e.g. in `HttpClientBuilder`)
* Add HTTP headers to message passing API
* Add HTTP headers to executor API (introduces `ExecutorSpiOptions4`)
* Add tests for Gradle, CLI, and pkl-executor invocations
* Improve documentation
* Add `isGlobPattern` API to class `String` for in-language validation
of http headers
* Behavior change: make sure explicitly configured `User-Agent` in
`HttpClientBuilder` can be shadowed by headers (allows users to set
`--http-header "**=User-Agent: My User Agent"` and for this to be the
only user agent).

CC @kyokuping
2026-05-21 20:07:06 -07:00

343 lines
11 KiB
Plaintext

//===----------------------------------------------------------------------===//
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// 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.
//===----------------------------------------------------------------------===//
/// Common settings for Pkl's own evaluator.
@ModuleInfo { minPklVersion = "0.32.0" }
@Since { version = "0.26.0" }
module pkl.EvaluatorSettings
/// The external properties available to Pkl, read using the `prop:` scheme.
externalProperties: Mapping<String, String>?
/// The environment variables available to Pkl, read using the `env:` scheme.
///
/// Example:
/// ```
/// env {
/// ["IS_PROD"] = "true"
/// }
/// ```
env: Mapping<String, String>?
/// The set of module URI patterns that can be imported.
///
/// Each element is a regular expression pattern that is tested against a module import.
///
/// Modules are imported either through an amends or extends clause, an import clause, or an
/// import expression.
///
/// Example:
/// ```
/// allowedModules {
/// "https:"
/// "file:"
/// "package:"
/// "projectpackage:"
/// }
/// ```
allowedModules: Listing<String(isRegex)>?
/// The set of resource URI patterns that can be imported.
///
/// Each element is a regular expression pattern that is tested against a resource read.
///
/// Example:
/// ```
/// allowedResources {
/// "https:"
/// "file:"
/// "package:"
/// "projectpackage:"
/// "env:"
/// "prop:"
/// }
/// ```
allowedResources: Listing<String(isRegex)>?
/// When to format messages with ANSI color codes.
///
/// Possible values:
///
/// - `"never"`: Never format
/// - `"auto"`: Format if the process' stdin, stdout, or stderr are connected to a console.
/// - `"always"`: Always format
@Since { version = "0.27.0" }
color: ("never" | "auto" | "always")?
/// Disables the file system cache for `package:` modules.
///
/// When caching is disabled, packages are loaded over the network and stored in memory.
noCache: Boolean?
/// A collection of jars, zips, or directories to be placed into the module path.
///
/// Module path modules and resources may be read and imported using the `modulepath:` scheme.
modulePath: Listing<String>?
/// The duration after which evaluation of a source module will be timed out.
///
/// Note that a timeout is treated the same as a program error in that any subsequent source modules will not be evaluated.
timeout: Duration?
/// The directory where `package:` modules are cached.
moduleCacheDir: String?
/// Restricts access to file-based modules and resources to those located under this directory.
rootDir: String?
/// Configuration of outgoing HTTP requests.
http: Http?
/// Configuration for external module reader processes.
@Since { version = "0.27.0" }
externalModuleReaders: Mapping<String, ExternalReader>?
/// Configuration for external resource reader processes.
@Since { version = "0.27.0" }
externalResourceReaders: Mapping<String, ExternalReader>?
/// Defines options for the formatting of calls to the trace() method.
///
/// Possible values:
///
/// - `"compact"`: All structures passed to trace() will be emitted on a single line.
/// - `"pretty"`: All structures passed to trace() will be indented and emitted across multiple lines.
@Since { version = "0.30.0" }
traceMode: ("compact" | "pretty")?
local const hostnameRegex = Regex(#"https?://([^/?#]*)"#)
local const hasNonEmptyHostname = (it: String) ->
let (hostname = hostnameRegex.findMatchesIn(it).getOrNull(0)?.groups?.getOrNull(1)?.value)
hostname != null && hostname.length > 0
/// A key or value in [Http.rewrites].
@Since { version = "0.29.0" }
typealias HttpRewrite = String(startsWith(Regex("https?://")), endsWith("/"), hasNonEmptyHostname)
/// Settings that control how Pkl talks to HTTP(S) servers.
class Http {
/// Configuration of the HTTP proxy to use.
proxy: Proxy?
/// Replace outbound requests from one URL with another URL.
///
/// Each key describes the prefix of a request, and each value describes the replacement prefix.
///
/// This can be useful for setting up mirroring of packages, which are fetched over HTTPS.
///
/// In the case of multiple matches, the longest prefix is used.
///
/// The URL hostname is case-insensitive.
///
/// A replacement only happens once when resolving URLs.
/// Therefore, the first replacement is the final URL that is used for the outbound request.
///
/// In the following example, an original request for `https://pkg.pkl-lang.org/my/pkg@1.0.0` is
/// replaced with `https://my.internal.mirror/my/pkg@1.0.0`.
///
/// This does not affect `3XX` status code redirect following.
///
/// Example:
///
/// ```
/// rewrites {
/// ["https://pkg.pkl-lang.org/"] = "https://my.internal.mirror/"
/// }
/// ```
///
/// Rewrite targets must satisfy the following:
///
/// * It starts with either `http://`, or `https://`.
/// * It ends with `/`.
/// * It has a non-empty hostname.
///
/// An rewrite target should also not contain a query string or fragment component
/// (not schematically enforced).
@Since { version = "0.29.0" }
rewrites: Mapping<HttpRewrite, HttpRewrite>?
/// HTTP headers to add to outbound requests.
///
/// Each key is a glob pattern, and each value is a mapping of header name to header value(s).
///
/// Before an HTTP request is made, each key is matched against the request URL.
/// If any matches are found, each of their described headers are added to the request.
///
/// To add headers to all HTTP requests, use `**` as the glob pattern.
///
/// Example:
///
/// ```
/// headers {
/// // Add this user agent to all out-bound requests.
/// ["**"] {
/// ["User-Agent"] = "MyApp-Pkl"
/// }
///
/// // Add an authorization header to all requests made to the
/// // `https://my.internal.service` host.
/// ["https://my.internal.service/**"] {
/// ["Authorization"] = "Bearer abc123"
/// }
/// }
/// ```
///
/// To repeat a header multiple times, set the value to a listing:
///
/// ```
/// headers {
/// // Adds:
/// // My-Custom-Header: foo
/// // My-Custom-Header: bar
/// ["**"] {
/// ["My-Custom-Header"] { "foo"; "bar" }
/// }
/// }
/// ```
///
/// For details on glob patterns, see
/// <https://pkl-lang.org/main/current/language-reference/index.html#glob-patterns>.
@Since { version = "0.32.0" }
headers: Mapping<String(isGlobPattern), HttpHeaders>?
}
/// Settings that control how Pkl talks to HTTP proxies.
class Proxy {
/// The proxy to use for HTTP(S) connections.
///
/// Only HTTP proxies are supported.
/// The proxy address must start with `http://`, and can only contain a host and a port.
/// If a port is omitted, it defaults to port `80`.
///
/// Proxy authentication is not supported.
///
/// Example:
/// ```
/// address = "http://my.proxy.example.com:5080"
/// ```
address: Uri(startsWith("http://"))?
/// Hosts to which all connections should bypass a proxy.
///
/// Values can be either hostnames, or IP addresses.
/// IP addresses can optionally be provided using
/// [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation).
///
/// The value `"*"` is a wildcard that disables proxying for all hosts.
///
/// A hostname matches all subdomains.
/// For example, `example.com` matches `foo.example.com`, but not `fooexample.com`.
/// A hostname that is prefixed with a dot matches the hostname itself,
/// so `.example.com` matches `example.com`.
///
/// Hostnames do not match their resolved IP addresses.
/// For example, the hostname `localhost` will not match `127.0.0.1`.
///
/// Optionally, a port can be specified.
/// If a port is omitted, all ports are matched.
///
/// Example:
///
/// ```
/// noProxy {
/// "127.0.0.1"
/// "169.254.0.0/16"
/// "example.com"
/// "localhost:5050"
/// }
/// ```
noProxy: Listing<String>(isDistinct)
}
@Since { version = "0.27.0" }
class ExternalReader {
/// The external reader executable.
///
/// Will be spawned with the same environment variables and working directory as the Pkl process.
/// Executable is resolved according to the operating system's process spawning rules.
/// On macOS, Linux, and Windows platforms, this may be:
///
/// * An absolute path
/// * A relative path (to the current working directory)
/// * The name of the executable, to be resolved against the `PATH` environment variable
executable: String
/// Additional command line arguments passed to the external reader process.
arguments: Listing<String>?
}
local typealias ReservedHttpHeaderName =
"connection"
| "content-length"
| "expect"
| "host"
| "keep-alive"
| "te"
| "trailer"
| "transfer-encoding"
| "upgrade"
local const reservedHttpHeaderPrefix =
Set(
"proxy-",
"sec-",
)
local const isNotReservedHeaderName = (header: String) ->
!(header.toLowerCase() is ReservedHttpHeaderName)
local const doesNotStartWithReservedPrefix = (header: String) ->
!reservedHttpHeaderPrefix.any((it) -> header.toLowerCase().startsWith(it))
local const hasValidHeaderNameSyntax = (header: String) ->
header.matches(Regex(#"[a-zA-Z0-9!#$%&'*+-.^_`|~]+"#))
/// A mapping of header names to header value(s).
///
/// The mapping must have distinct header keys (case insensitive).
@Since { version = "0.32.0" }
typealias HttpHeaders =
Mapping<HttpHeaderName, *Listing<HttpHeaderValue> | HttpHeaderValue>(
keys.toList().isDistinctBy((it) -> it.toLowerCase()),
)
/// An HTTP header name.
///
/// It conforms to the following rules:
///
/// * It is not one of "connection", "keep-alive", "content-length", "expect", "host", "upgrade",
/// "te", "transfer-encoding", "trailer" (case insensitive).
/// * It does not start with "proxy-" nor "sec-".
/// * It consists of a-z, A-Z, or a character in "!#$%&'*+-.^_`|~".
/// * It is not a blank string.
@Since { version = "0.32.0" }
typealias HttpHeaderName =
String(
isNotReservedHeaderName,
doesNotStartWithReservedPrefix,
hasValidHeaderNameSyntax,
isNotBlank,
)
/// An HTTP header value.
///
/// It consists of ASCII characters between the range of 0x20 - 0x7E, and 0x80 - 0xFF (all visible
/// ASCII characters), and also the tab character.
@Since { version = "0.32.0" }
typealias HttpHeaderValue =
String(matches(Regex(#"[\t\u0020-\u007E\u0080-\u00FF]*"#)), length < 4096)