vendor: update buildkit to v0.32.0-rc1
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
-1
@@ -3,5 +3,4 @@
|
||||
.idea
|
||||
.env
|
||||
.mcp.json
|
||||
.claude/
|
||||
settings.local.json
|
||||
|
||||
+3
-3
@@ -4,12 +4,12 @@
|
||||
|
||||
| Total Contributors | Total Contributions |
|
||||
| --- | --- |
|
||||
| 13 | 110 |
|
||||
| 13 | 115 |
|
||||
|
||||
| Username | All Time Contribution Count | All Commits |
|
||||
| --- | --- | --- |
|
||||
| @casualjim | 58 | <https://github.com/go-openapi/errors/commits?author=casualjim> |
|
||||
| @fredbi | 36 | <https://github.com/go-openapi/errors/commits?author=fredbi> |
|
||||
| @fredbi | 41 | <https://github.com/go-openapi/errors/commits?author=fredbi> |
|
||||
| @youyuanwu | 5 | <https://github.com/go-openapi/errors/commits?author=youyuanwu> |
|
||||
| @alexandear | 2 | <https://github.com/go-openapi/errors/commits?author=alexandear> |
|
||||
| @fiorix | 1 | <https://github.com/go-openapi/errors/commits?author=fiorix> |
|
||||
@@ -22,4 +22,4 @@
|
||||
| @aokumasan | 1 | <https://github.com/go-openapi/errors/commits?author=aokumasan> |
|
||||
| @ujjwalsh | 1 | <https://github.com/go-openapi/errors/commits?author=ujjwalsh> |
|
||||
|
||||
_this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_
|
||||
_this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ Maintainers can cut a new release by either:
|
||||
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
|
||||
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
|
||||
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
|
||||
[discord-url]: https://discord.gg/twZ9BwT3
|
||||
[discord-url]: https://discord.gg/FfnFYaC3k5
|
||||
|
||||
<!-- Badges: license & compliance -->
|
||||
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
|
||||
|
||||
+1
@@ -3,3 +3,4 @@
|
||||
.idea
|
||||
.env
|
||||
.mcp.json
|
||||
.worktrees
|
||||
|
||||
+2
@@ -7,6 +7,8 @@ linters:
|
||||
- gochecknoglobals # on this repo, it is hard to refactor without globals/inits and no breaking change
|
||||
- gochecknoinits
|
||||
- godox
|
||||
- gomodguard
|
||||
- gomodguard_v2
|
||||
- exhaustruct
|
||||
- nlreturn
|
||||
- nonamedreturns
|
||||
|
||||
+3
-3
@@ -4,12 +4,12 @@
|
||||
|
||||
| Total Contributors | Total Contributions |
|
||||
| --- | --- |
|
||||
| 14 | 123 |
|
||||
| 14 | 133 |
|
||||
|
||||
| Username | All Time Contribution Count | All Commits |
|
||||
| --- | --- | --- |
|
||||
| @fredbi | 55 | <https://github.com/go-openapi/loads/commits?author=fredbi> |
|
||||
| @casualjim | 48 | <https://github.com/go-openapi/loads/commits?author=casualjim> |
|
||||
| @fredbi | 45 | <https://github.com/go-openapi/loads/commits?author=fredbi> |
|
||||
| @youyuanwu | 6 | <https://github.com/go-openapi/loads/commits?author=youyuanwu> |
|
||||
| @vburenin | 4 | <https://github.com/go-openapi/loads/commits?author=vburenin> |
|
||||
| @keramix | 4 | <https://github.com/go-openapi/loads/commits?author=keramix> |
|
||||
@@ -23,4 +23,4 @@
|
||||
| @kreativka | 1 | <https://github.com/go-openapi/loads/commits?author=kreativka> |
|
||||
| @petrkotas | 1 | <https://github.com/go-openapi/loads/commits?author=petrkotas> |
|
||||
|
||||
_this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_
|
||||
_this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_
|
||||
|
||||
+45
-10
@@ -20,12 +20,9 @@ Supports JSON and YAML documents.
|
||||
|
||||
* **2025-12-19** : new community chat on discord
|
||||
* a new discord community channel is available to be notified of changes and support users
|
||||
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
|
||||
|
||||
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
|
||||
|
||||
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
|
||||
|
||||
## Status
|
||||
|
||||
API is stable.
|
||||
@@ -58,6 +55,41 @@ go get github.com/go-openapi/loads
|
||||
|
||||
See also the provided [examples](https://pkg.go.dev/github.com/go-openapi/loads#pkg-examples).
|
||||
|
||||
## Security
|
||||
|
||||
This library does not enforce a security policy of its own: it reads whatever the configured
|
||||
loader is allowed to read.
|
||||
|
||||
This is deliberate — like `go-openapi/swag/loading`, it is a base utility,
|
||||
and sanitizing or containing untrusted input is the caller's responsibility,
|
||||
just as sanitizing a file name before passing it to `os.ReadFile` is not that function's job.
|
||||
|
||||
When a spec — its path or its `$ref` contents — may come from an untrusted source, confine
|
||||
loading explicitly (e.g. `loading.WithRoot` for local files and a restricted
|
||||
`loading.WithHTTPClient` for remote URLs, passed via `loads.WithLoadingOptions`).
|
||||
|
||||
For the common case, the pre-baked `loads.SpecRestricted` / `loads.JSONSpecRestricted` loaders
|
||||
bundle a trusted root with a network-restricted client (`loads.RestrictedHTTPClient`) and apply
|
||||
the confinement to `$ref` resolution as well:
|
||||
|
||||
```go
|
||||
doc, err := loads.SpecRestricted(path, trustedRoot)
|
||||
```
|
||||
|
||||
To harden the package-level default in one call — so even callers that rely on the global
|
||||
loader (including cross-package `$ref` resolution via `spec.PathLoader`) are confined, with no
|
||||
unconfined fallback left — use `loads.SetRestrictedLoaders` at startup:
|
||||
|
||||
```go
|
||||
loads.SetRestrictedLoaders(trustedRoot)
|
||||
```
|
||||
|
||||
Note that `loads.AddLoader` only *prepends* to the default chain, leaving the unconfined loader
|
||||
reachable; use `loads.SetLoaders` / `loads.SetRestrictedLoaders` to replace it.
|
||||
|
||||
See the [Security section of the package documentation][security-doc] for the threat model and
|
||||
runnable examples. For the project's vulnerability reporting policy, see [SECURITY.md](./SECURITY.md).
|
||||
|
||||
## Change log
|
||||
|
||||
See <https://github.com/go-openapi/loads/releases>
|
||||
@@ -69,9 +101,9 @@ This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE).
|
||||
## Other documentation
|
||||
|
||||
* [All-time contributors](./CONTRIBUTORS.md)
|
||||
* [Contributing guidelines](.github/CONTRIBUTING.md)
|
||||
* [Maintainers documentation](docs/MAINTAINERS.md)
|
||||
* [Code style](docs/STYLE.md)
|
||||
* [Contributing guidelines][contributing-doc-site]
|
||||
* [Maintainers documentation][maintainers-doc-site]
|
||||
* [Code style][style-doc-site]
|
||||
|
||||
## Cutting a new release
|
||||
|
||||
@@ -102,11 +134,8 @@ Maintainers can cut a new release by either:
|
||||
<!-- Badges: documentation & support -->
|
||||
[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/loads
|
||||
[godoc-url]: http://pkg.go.dev/github.com/go-openapi/loads
|
||||
[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
|
||||
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
|
||||
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
|
||||
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
|
||||
[discord-url]: https://discord.gg/twZ9BwT3
|
||||
[discord-url]: https://discord.gg/FfnFYaC3k5
|
||||
|
||||
<!-- Badges: license & compliance -->
|
||||
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
|
||||
@@ -116,3 +145,9 @@ Maintainers can cut a new release by either:
|
||||
[goversion-url]: https://github.com/go-openapi/loads/blob/master/go.mod
|
||||
[top-badge]: https://img.shields.io/github/languages/top/go-openapi/loads
|
||||
[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/loads/latest
|
||||
<!-- Documentation links -->
|
||||
[security-doc]: https://pkg.go.dev/github.com/go-openapi/loads#hdr-Security
|
||||
<!-- Organization docs -->
|
||||
[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html
|
||||
[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html
|
||||
[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html
|
||||
|
||||
+68
@@ -6,4 +6,72 @@
|
||||
// It is used by other go-openapi packages to load and run analysis on local or remote spec documents.
|
||||
//
|
||||
// Loaders support JSON and YAML documents.
|
||||
//
|
||||
// # Security
|
||||
//
|
||||
// This package does not enforce a security policy of its own: like the underlying
|
||||
// [github.com/go-openapi/swag/loading] utilities, it reads whatever the configured loader is
|
||||
// allowed to read.
|
||||
//
|
||||
// When a spec — its path or its contents — may derive from untrusted input, the caller must confine loading explicitly.
|
||||
//
|
||||
// This is a deliberate design choice.
|
||||
// Both this package and the [github.com/go-openapi/swag/loading] utilities are base building blocks:
|
||||
// deciding which sources are legitimate, and containing access to them,
|
||||
// requires application context that a general-purpose loader does not have.
|
||||
//
|
||||
// Just as sanitizing a file name before handing it to [os.ReadFile] is the caller's
|
||||
// responsibility and not that function's, sanitizing and containing the path and references
|
||||
// resolved here is the responsibility of the code that may feed them untrusted input.
|
||||
//
|
||||
// There are two distinct attack surfaces:
|
||||
//
|
||||
// - The path passed to [Spec], [JSONSpec], or [Embedded]. By default a local path is read
|
||||
// with no confinement, so a caller-controlled path (including an absolute path or a
|
||||
// "file:///etc/passwd" URI) may read any file the process can access. A remote path is
|
||||
// fetched with [net/http.DefaultClient], which follows redirects and performs no
|
||||
// destination filtering, so a caller-controlled URL may reach internal services or cloud
|
||||
// metadata endpoints (server-side request forgery).
|
||||
//
|
||||
// - The contents of the spec, when references are resolved. [Document.Expanded] follows the
|
||||
// "$ref" pointers found inside the document by calling the same loader recursively. A spec
|
||||
// obtained even from a trusted path can therefore drive arbitrary local reads
|
||||
// ("$ref": "file:///etc/passwd") or SSRF ("$ref": "http://169.254.169.254/...") through
|
||||
// its own contents. This amplification is specific to reference resolution and does not
|
||||
// exist in the raw loading utilities.
|
||||
//
|
||||
// Mitigation. Pass [github.com/go-openapi/swag/loading] options through [WithLoadingOptions];
|
||||
// they are attached to the document's loader and so apply both to the initial load and to
|
||||
// every "$ref" resolved during expansion:
|
||||
//
|
||||
// - [github.com/go-openapi/swag/loading.WithRoot] confines local reads to a trusted
|
||||
// directory, rejecting absolute paths, ".." traversal, and symlinks that escape it. Prefer
|
||||
// it over a [github.com/go-openapi/swag/loading.WithFS] built from [os.DirFS], which does
|
||||
// not block symlink escapes.
|
||||
//
|
||||
// - [github.com/go-openapi/swag/loading.WithHTTPClient] allows to supply a restricted HTTP client.
|
||||
// Enforce the network policy at dial time (a [net.Dialer] Control hook), so it also covers
|
||||
// redirects and DNS rebinding, which a URL-string allowlist cannot. See the example on
|
||||
// [Spec].
|
||||
//
|
||||
// Pre-baked loaders. When the opinionated defaults fit, [SpecRestricted], [JSONSpecRestricted]
|
||||
// and [JSONDocRestricted] bundle a trusted root with a network-restricted client
|
||||
// ([RestrictedHTTPClient]), and apply the confinement to "$ref" resolution as well — so the
|
||||
// common case needs no manual wiring. To harden the global default in one call (so even callers
|
||||
// that rely on the package-level loader are confined), use [SetRestrictedLoaders]. Reach for the
|
||||
// options above when you need a custom policy; [IsForbiddenAddress] exposes the default network
|
||||
// policy so you can reuse it as the base of your own HTTP client.
|
||||
//
|
||||
// Caveats:
|
||||
//
|
||||
// - The package-level default loader (also installed as [github.com/go-openapi/spec.PathLoader])
|
||||
// carries no loading options and is therefore unconfined. It is used as a fallback when
|
||||
// expansion runs without a document loader, and by other go-openapi packages that resolve
|
||||
// references on their own. [AddLoader] does not fix this — it only prepends, leaving the
|
||||
// unconfined fallback reachable. Either build a confined loader per call, or replace the
|
||||
// global default outright with [SetLoaders] / [SetRestrictedLoaders].
|
||||
//
|
||||
// - A custom loader installed via [WithDocLoader] or [AddLoader] only honors these
|
||||
// protections if its loading function actually applies the [github.com/go-openapi/swag/loading]
|
||||
// options it is given.
|
||||
package loads
|
||||
|
||||
+4
@@ -15,4 +15,8 @@ const (
|
||||
|
||||
// ErrNoLoader indicates that no configured loader matched the input.
|
||||
ErrNoLoader loaderError = "no loader matched"
|
||||
|
||||
// ErrForbiddenAddress is returned by [RestrictedHTTPClient] when a connection is attempted
|
||||
// to a non-public address (loopback, private, link-local, or unspecified).
|
||||
ErrForbiddenAddress loaderError = "blocked dial to a non-public address"
|
||||
)
|
||||
|
||||
+76
-3
@@ -21,6 +21,15 @@ import (
|
||||
var loaders *loader
|
||||
|
||||
func init() {
|
||||
loaders = defaultLoaders()
|
||||
|
||||
// sets the global default loader for go-openapi/spec
|
||||
spec.PathLoader = loaders.Load
|
||||
}
|
||||
|
||||
// defaultLoaders builds the built-in loader chain: a YAML matcher first, with a JSON loader as
|
||||
// the catch-all fallback.
|
||||
func defaultLoaders() *loader {
|
||||
jsonLoader := &loader{
|
||||
DocLoaderWithMatch: DocLoaderWithMatch{
|
||||
Match: func(_ string) bool {
|
||||
@@ -30,15 +39,35 @@ func init() {
|
||||
},
|
||||
}
|
||||
|
||||
loaders = jsonLoader.WithHead(&loader{
|
||||
return jsonLoader.WithHead(&loader{
|
||||
DocLoaderWithMatch: DocLoaderWithMatch{
|
||||
Match: loading.YAMLMatcher,
|
||||
Fn: loading.YAMLDoc,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// sets the global default loader for go-openapi/spec
|
||||
spec.PathLoader = loaders.Load
|
||||
// buildLoaderChain links a list of [DocLoaderWithMatch] into a loader chain, preserving order.
|
||||
// Entries with a nil Fn are skipped. Returns nil when no usable loader is provided.
|
||||
func buildLoaderChain(ldrs ...DocLoaderWithMatch) *loader {
|
||||
var final, prev *loader
|
||||
for _, ldr := range ldrs {
|
||||
if ldr.Fn == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
node := &loader{DocLoaderWithMatch: ldr}
|
||||
if prev == nil {
|
||||
final = node
|
||||
prev = node
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
prev = prev.WithNext(node)
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
// DocLoader represents a doc loader type.
|
||||
@@ -141,6 +170,17 @@ func JSONDoc(path string, opts ...loading.Option) (json.RawMessage, error) {
|
||||
//
|
||||
// This function updates the default loader used by [github.com/go-openapi/spec].
|
||||
// Since this sets package level globals, you shouldn't call this concurrently.
|
||||
//
|
||||
// # Security
|
||||
//
|
||||
// AddLoader only *prepends* to the default chain: the previous loaders — including the
|
||||
// unconfined JSON fallback — remain reachable, both here and via cross-package "$ref"
|
||||
// resolution. It is therefore the wrong tool for hardening the global default. To replace the
|
||||
// chain entirely (leaving no unconfined fallback) use [SetLoaders], or [SetRestrictedLoaders]
|
||||
// for a one-call confined setup. For a single load, prefer a confined per-call loader via
|
||||
// [WithLoadingOptions] or [WithDocLoaderMatches]. A custom loader registered here only honors
|
||||
// the protections if its loading function applies the [github.com/go-openapi/swag/loading]
|
||||
// options it is given. See the package documentation on Security.
|
||||
func AddLoader(predicate DocMatcher, load DocLoader) {
|
||||
loaders = loaders.WithHead(&loader{
|
||||
DocLoaderWithMatch: DocLoaderWithMatch{
|
||||
@@ -152,3 +192,36 @@ func AddLoader(predicate DocMatcher, load DocLoader) {
|
||||
// sets the global default loader for go-openapi/spec
|
||||
spec.PathLoader = loaders.Load
|
||||
}
|
||||
|
||||
// SetLoaders replaces the package-level default loader chain with the given loaders, tried in
|
||||
// order, and re-points [github.com/go-openapi/spec.PathLoader] at it.
|
||||
//
|
||||
// Unlike [AddLoader], nothing of the previous default survives — so when the replacement is
|
||||
// confined, no unconfined fallback remains for any caller relying on the global default
|
||||
// (including cross-package "$ref" resolution). An entry with a nil Match is a catch-all; you
|
||||
// are responsible for providing a suitable fallback. Calling SetLoaders with no usable loader
|
||||
// restores the built-in default (a YAML matcher with a JSON fallback).
|
||||
//
|
||||
// # Concurrency
|
||||
//
|
||||
// This sets package-level globals and the [github.com/go-openapi/spec] global loader. It is
|
||||
// not safe to call concurrently with other loads or with [AddLoader]; configure it once at
|
||||
// startup, before serving.
|
||||
//
|
||||
// # Security
|
||||
//
|
||||
// This is the way to harden the global default in one place. For a ready-made confined setup,
|
||||
// see [SetRestrictedLoaders]. As with [AddLoader], a custom loader only honors the protections
|
||||
// if its loading function applies the [github.com/go-openapi/swag/loading] options it is given.
|
||||
// See the package documentation on Security.
|
||||
func SetLoaders(ldrs ...DocLoaderWithMatch) {
|
||||
chain := buildLoaderChain(ldrs...)
|
||||
if chain == nil {
|
||||
chain = defaultLoaders()
|
||||
}
|
||||
|
||||
loaders = chain
|
||||
|
||||
// sets the global default loader for go-openapi/spec
|
||||
spec.PathLoader = loaders.Load
|
||||
}
|
||||
|
||||
+6
-15
@@ -51,25 +51,16 @@ func WithDocLoader(l DocLoader) LoaderOption {
|
||||
// Loaders are executed in the order of provided [DocLoaderWithMatch] 'es.
|
||||
func WithDocLoaderMatches(l ...DocLoaderWithMatch) LoaderOption {
|
||||
return func(opt *options) {
|
||||
var final, prev *loader
|
||||
for _, ldr := range l {
|
||||
if ldr.Fn == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if prev == nil {
|
||||
final = &loader{DocLoaderWithMatch: ldr}
|
||||
prev = final
|
||||
continue
|
||||
}
|
||||
|
||||
prev = prev.WithNext(&loader{DocLoaderWithMatch: ldr})
|
||||
}
|
||||
opt.loader = final
|
||||
opt.loader = buildLoaderChain(l...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithLoadingOptions adds some [loading.Option] to be added when calling a registered loader.
|
||||
//
|
||||
// The options are attached to the document's loader, so they apply both to the initial load
|
||||
// and to every "$ref" resolved during [Document.Expanded]. This is the recommended place to
|
||||
// confine loading of untrusted input, for example with [loading.WithRoot] (local) and
|
||||
// [loading.WithHTTPClient] (remote). See the package documentation on Security.
|
||||
func WithLoadingOptions(loadingOptions ...loading.Option) LoaderOption {
|
||||
return func(opt *options) {
|
||||
opt.loadingOptions = loadingOptions
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package loads
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/swag/loading"
|
||||
)
|
||||
|
||||
const (
|
||||
// numConfinementOptions is the count of loading options appended to enforce confinement
|
||||
// (WithRoot + WithHTTPClient), used to size the bundled option slice.
|
||||
numConfinementOptions = 2
|
||||
|
||||
defaultTLSHandshakeTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// RestrictedHTTPClient returns an [http.Client] that refuses, at dial time, to connect to
|
||||
// loopback, private, link-local (including cloud-metadata endpoints such as 169.254.169.254),
|
||||
// or unspecified addresses. A blocked connection fails with an error wrapping
|
||||
// [ErrForbiddenAddress].
|
||||
//
|
||||
// The check runs in the dialer Control hook, after DNS resolution and before connect, so it
|
||||
// also covers HTTP redirects and DNS rebinding — which a URL-string allowlist cannot. The
|
||||
// client does not honor proxy environment variables, so the guard always inspects the real
|
||||
// destination rather than a proxy address.
|
||||
//
|
||||
// This is the network half of the restricted loaders ([JSONDocRestricted],
|
||||
// [JSONSpecRestricted], [SpecRestricted]). It may also be used directly with
|
||||
// [github.com/go-openapi/swag/loading.WithHTTPClient].
|
||||
//
|
||||
// The policy is opinionated and deliberately simple. For a different one (a custom allow/deny
|
||||
// list, an explicit proxy, mutual TLS, ...), build your own client and pass it with
|
||||
// [github.com/go-openapi/swag/loading.WithHTTPClient]. To keep the default address policy as a
|
||||
// base, reuse [IsForbiddenAddress] in your own dialer Control hook — see the package examples
|
||||
// for the pattern.
|
||||
func RestrictedHTTPClient() *http.Client {
|
||||
control := func(_, address string, _ syscall.RawConn) error {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if IsForbiddenAddress(addr) {
|
||||
return ErrForbiddenAddress
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: nil, // dial the real destination so the guard inspects it
|
||||
DialContext: (&net.Dialer{Control: control}).DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: defaultTLSHandshakeTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsForbiddenAddress reports whether addr is one that [RestrictedHTTPClient] refuses to dial:
|
||||
// a loopback, private, link-local (including cloud-metadata endpoints such as 169.254.169.254),
|
||||
// or unspecified address. IPv4-mapped IPv6 addresses are unmapped before the check.
|
||||
//
|
||||
// It is exported so callers can reuse or extend the default policy when building their own
|
||||
// dialer Control hook, for example to also reject a CGNAT range or to carve out a single
|
||||
// trusted internal host:
|
||||
//
|
||||
// control := func(_, address string, _ syscall.RawConn) error {
|
||||
// host, _, err := net.SplitHostPort(address)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// addr, err := netip.ParseAddr(host)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if loads.IsForbiddenAddress(addr) && host != allowedInternalHost {
|
||||
// return loads.ErrForbiddenAddress
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
func IsForbiddenAddress(addr netip.Addr) bool {
|
||||
a := addr.Unmap()
|
||||
|
||||
return a.IsLoopback() || a.IsPrivate() || a.IsLinkLocalUnicast() || a.IsUnspecified()
|
||||
}
|
||||
|
||||
// restrictedLoadingOptions bundles caller-supplied options with the confinement options,
|
||||
// appended last so that local rooting and the restricted client always take precedence
|
||||
// (the loading options are last-wins).
|
||||
func restrictedLoadingOptions(root string, extra []loading.Option) []loading.Option {
|
||||
out := make([]loading.Option, 0, len(extra)+numConfinementOptions)
|
||||
out = append(out, extra...)
|
||||
out = append(out, loading.WithRoot(root), loading.WithHTTPClient(RestrictedHTTPClient()))
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// JSONDocRestricted returns a JSON [DocLoader] that confines local reads to root (via
|
||||
// [github.com/go-openapi/swag/loading.WithRoot]) and restricts remote fetches with
|
||||
// [RestrictedHTTPClient].
|
||||
//
|
||||
// The returned loader may be registered with [WithDocLoader] or [AddLoader]. The confinement
|
||||
// always takes precedence over any option passed here or at call time, so a caller cannot
|
||||
// loosen it through [WithLoadingOptions].
|
||||
//
|
||||
// Like [JSONDoc], it loads JSON only: it does not convert YAML. For specs whose references may
|
||||
// point at YAML documents, prefer [SpecRestricted], which keeps the default JSON/YAML chain.
|
||||
func JSONDocRestricted(root string, opts ...loading.Option) DocLoader {
|
||||
// one restricted client, reused for every path and $ref
|
||||
return restrictedDocLoader(JSONDoc, restrictedLoadingOptions(root, opts))
|
||||
}
|
||||
|
||||
// restrictedDocLoader wraps a [DocLoader] so that the confinement options in base are always
|
||||
// applied, appended after any call-time options so they take precedence (loading options are
|
||||
// last-wins).
|
||||
func restrictedDocLoader(fn DocLoader, base []loading.Option) DocLoader {
|
||||
return func(path string, callOpts ...loading.Option) (json.RawMessage, error) {
|
||||
if len(callOpts) == 0 {
|
||||
return fn(path, base...)
|
||||
}
|
||||
|
||||
all := make([]loading.Option, 0, len(callOpts)+len(base))
|
||||
all = append(all, callOpts...)
|
||||
all = append(all, base...) // confinement (tail of base) still wins
|
||||
|
||||
return fn(path, all...)
|
||||
}
|
||||
}
|
||||
|
||||
// JSONSpecRestricted loads a JSON spec like [JSONSpec], but confines local reads to root and
|
||||
// restricts remote fetches with [RestrictedHTTPClient].
|
||||
//
|
||||
// The confinement is attached to the document's loader, so it also applies to every "$ref"
|
||||
// resolved by [Document.Expanded]. Extra [github.com/go-openapi/swag/loading] options (custom
|
||||
// headers, basic auth, timeout, ...) may be supplied; the confinement always wins over them.
|
||||
func JSONSpecRestricted(path, root string, opts ...loading.Option) (*Document, error) {
|
||||
return JSONSpec(path, WithLoadingOptions(restrictedLoadingOptions(root, opts)...))
|
||||
}
|
||||
|
||||
// SpecRestricted loads a spec like [Spec] — with JSON/YAML auto-detection — but confines local
|
||||
// reads to root and restricts remote fetches with [RestrictedHTTPClient].
|
||||
//
|
||||
// The confinement is attached to the document's loader, so it also applies to every "$ref"
|
||||
// resolved by [Document.Expanded]. Extra [github.com/go-openapi/swag/loading] options (custom
|
||||
// headers, basic auth, timeout, ...) may be supplied; the confinement always wins over them.
|
||||
func SpecRestricted(path, root string, opts ...loading.Option) (*Document, error) {
|
||||
return Spec(path, WithLoadingOptions(restrictedLoadingOptions(root, opts)...))
|
||||
}
|
||||
|
||||
// SetRestrictedLoaders hardens the package-level default in a single call: it installs a
|
||||
// confined JSON/YAML loader chain — local reads rooted at root, remote fetches through
|
||||
// [RestrictedHTTPClient] — as the global default and as
|
||||
// [github.com/go-openapi/spec.PathLoader].
|
||||
//
|
||||
// After this call, every load that relies on the package default ([Spec], [JSONSpec], and any
|
||||
// cross-package "$ref" resolution) is confined, with no unconfined fallback left behind. It is
|
||||
// the global counterpart of [SpecRestricted]; a single restricted client is shared across the
|
||||
// chain. Extra [github.com/go-openapi/swag/loading] options may be supplied; the confinement
|
||||
// always wins over them.
|
||||
//
|
||||
// # Concurrency
|
||||
//
|
||||
// Like [SetLoaders], this mutates package-level and [github.com/go-openapi/spec] globals and is
|
||||
// not safe to call concurrently. Configure it once at startup, before serving. To revert, call
|
||||
// [SetLoaders] with no arguments.
|
||||
func SetRestrictedLoaders(root string, opts ...loading.Option) {
|
||||
base := restrictedLoadingOptions(root, opts) // one restricted client shared by the whole chain
|
||||
|
||||
SetLoaders(
|
||||
NewDocLoaderWithMatch(restrictedDocLoader(loading.YAMLDoc, base), loading.YAMLMatcher),
|
||||
NewDocLoaderWithMatch(restrictedDocLoader(JSONDoc, base), nil), // nil matcher: JSON catch-all fallback
|
||||
)
|
||||
}
|
||||
+16
@@ -77,6 +77,14 @@ func Embedded(orig, flat json.RawMessage, opts ...LoaderOption) (*Document, erro
|
||||
// Spec loads a new spec document from a local or remote path.
|
||||
//
|
||||
// By default it uses a JSON or YAML loader, with auto-detection based on the resource extension.
|
||||
//
|
||||
// Security: by default the path is read with no confinement (local) and fetched with
|
||||
// [net/http.DefaultClient] (remote), and any "$ref" later resolved by [Document.Expanded] is
|
||||
// loaded the same way. When the path or the spec contents may derive from untrusted input,
|
||||
// confine loading with [WithLoadingOptions] (for example
|
||||
// [github.com/go-openapi/swag/loading.WithRoot] and
|
||||
// [github.com/go-openapi/swag/loading.WithHTTPClient]). See the package documentation on
|
||||
// Security.
|
||||
func Spec(path string, opts ...LoaderOption) (*Document, error) {
|
||||
ldr := loaderFromOptions(opts)
|
||||
|
||||
@@ -157,6 +165,14 @@ func trimData(in json.RawMessage) (json.RawMessage, error) {
|
||||
}
|
||||
|
||||
// Expanded expands the $ref fields in the spec [Document] and returns a new expanded [Document].
|
||||
//
|
||||
// Security: expansion resolves every "$ref" by calling the document's loader recursively, so
|
||||
// the spec contents drive further loads. A spec from an untrusted source can thus trigger
|
||||
// arbitrary local reads or SSRF through its references. The loader carries the
|
||||
// [github.com/go-openapi/swag/loading] options supplied via [WithLoadingOptions] at load time;
|
||||
// configure confinement there so it applies to expansion as well. When no document loader is
|
||||
// set, expansion falls back to the unconfined package-level loader. See the package
|
||||
// documentation on Security.
|
||||
func (d *Document) Expanded(options ...*spec.ExpandOptions) (*Document, error) {
|
||||
swspec := new(spec.Swagger)
|
||||
if err := json.Unmarshal(d.raw, swspec); err != nil {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ linters:
|
||||
dupl:
|
||||
threshold: 200
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-len: 9
|
||||
min-occurrences: 3
|
||||
cyclop:
|
||||
max-complexity: 25
|
||||
|
||||
+2
-2
@@ -4,12 +4,12 @@
|
||||
|
||||
| Total Contributors | Total Contributions |
|
||||
| --- | --- |
|
||||
| 71 | 565 |
|
||||
| 71 | 567 |
|
||||
|
||||
| Username | All Time Contribution Count | All Commits |
|
||||
| --- | --- | --- |
|
||||
| @casualjim | 268 | <https://github.com/go-openapi/runtime/commits?author=casualjim> |
|
||||
| @fredbi | 140 | <https://github.com/go-openapi/runtime/commits?author=fredbi> |
|
||||
| @fredbi | 142 | <https://github.com/go-openapi/runtime/commits?author=fredbi> |
|
||||
| @youyuanwu | 19 | <https://github.com/go-openapi/runtime/commits?author=youyuanwu> |
|
||||
| @josephwoodward | 13 | <https://github.com/go-openapi/runtime/commits?author=josephwoodward> |
|
||||
| @kenjones-cisco | 12 | <https://github.com/go-openapi/runtime/commits?author=kenjones-cisco> |
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ func (o *UntypedRequestBinder) bind(request *http.Request, routeParams RoutePara
|
||||
if isMap {
|
||||
tpe := binder.Type()
|
||||
if tpe == nil {
|
||||
if param.Schema.Type.Contains(typeArray) {
|
||||
if param.Schema != nil && param.Schema.Type.Contains(typeArray) {
|
||||
tpe = reflect.TypeFor[[]any]()
|
||||
} else {
|
||||
tpe = reflect.TypeFor[map[string]any]()
|
||||
|
||||
+3
@@ -4,7 +4,10 @@ linters:
|
||||
disable:
|
||||
- depguard
|
||||
- funlen
|
||||
- goconst
|
||||
- godox
|
||||
- gomodguard
|
||||
- gomodguard_v2
|
||||
- exhaustruct
|
||||
- nlreturn
|
||||
- nonamedreturns
|
||||
|
||||
+2
-2
@@ -4,12 +4,12 @@
|
||||
|
||||
| Total Contributors | Total Contributions |
|
||||
| --- | --- |
|
||||
| 38 | 396 |
|
||||
| 38 | 398 |
|
||||
|
||||
| Username | All Time Contribution Count | All Commits |
|
||||
| --- | --- | --- |
|
||||
| @casualjim | 191 | <https://github.com/go-openapi/spec/commits?author=casualjim> |
|
||||
| @fredbi | 94 | <https://github.com/go-openapi/spec/commits?author=fredbi> |
|
||||
| @fredbi | 96 | <https://github.com/go-openapi/spec/commits?author=fredbi> |
|
||||
| @pytlesk4 | 26 | <https://github.com/go-openapi/spec/commits?author=pytlesk4> |
|
||||
| @kul-amr | 10 | <https://github.com/go-openapi/spec/commits?author=kul-amr> |
|
||||
| @keramix | 10 | <https://github.com/go-openapi/spec/commits?author=keramix> |
|
||||
|
||||
+7
-9
@@ -18,12 +18,9 @@ The object model for OpenAPI v2 specification documents.
|
||||
|
||||
* **2025-12-19** : new community chat on discord
|
||||
* a new discord community channel is available to be notified of changes and support users
|
||||
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
|
||||
|
||||
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
|
||||
|
||||
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
|
||||
|
||||
## Status
|
||||
|
||||
API is stable.
|
||||
@@ -95,9 +92,9 @@ This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE).
|
||||
## Other documentation
|
||||
|
||||
* [All-time contributors](./CONTRIBUTORS.md)
|
||||
* [Contributing guidelines](.github/CONTRIBUTING.md)
|
||||
* [Maintainers documentation](docs/MAINTAINERS.md)
|
||||
* [Code style](docs/STYLE.md)
|
||||
* [Contributing guidelines][contributing-doc-site]
|
||||
* [Maintainers documentation][maintainers-doc-site]
|
||||
* [Code style][style-doc-site]
|
||||
|
||||
## Cutting a new release
|
||||
|
||||
@@ -132,9 +129,6 @@ Maintainers can cut a new release by either:
|
||||
[doc-url]: https://goswagger.io/go-openapi
|
||||
[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/spec
|
||||
[godoc-url]: http://pkg.go.dev/github.com/go-openapi/spec
|
||||
[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
|
||||
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
|
||||
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
|
||||
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
|
||||
[discord-url]: https://discord.gg/FfnFYaC3k5
|
||||
|
||||
@@ -146,3 +140,7 @@ Maintainers can cut a new release by either:
|
||||
[goversion-url]: https://github.com/go-openapi/spec/blob/master/go.mod
|
||||
[top-badge]: https://img.shields.io/github/languages/top/go-openapi/spec
|
||||
[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/spec/latest
|
||||
<!-- Organization docs -->
|
||||
[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html
|
||||
[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html
|
||||
[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html
|
||||
|
||||
+5
-1
@@ -150,7 +150,11 @@ func (h Header) MarshalJSON() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonutils.ConcatJSON(b1, b2, b3), nil
|
||||
b4, err := json.Marshal(h.VendorExtensible)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonutils.ConcatJSON(b1, b2, b3, b4), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals this header from JSON.
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ func (r *schemaLoader) updateBasePath(transitive *schemaLoader, basePath string)
|
||||
|
||||
func (r *schemaLoader) resolveRef(ref *Ref, target any, basePath string) error {
|
||||
tgt := reflect.ValueOf(target)
|
||||
if tgt.Kind() != reflect.Ptr {
|
||||
if tgt.Kind() != reflect.Pointer {
|
||||
return ErrResolveRefNeedsAPointer
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ version: "2"
|
||||
linters:
|
||||
default: all
|
||||
disable:
|
||||
- goconst # has become too noisy. Disabled
|
||||
- depguard
|
||||
- funlen
|
||||
- gomoddirectives
|
||||
|
||||
+2
-2
@@ -4,12 +4,12 @@
|
||||
|
||||
| Total Contributors | Total Contributions |
|
||||
| --- | --- |
|
||||
| 40 | 234 |
|
||||
| 40 | 239 |
|
||||
|
||||
| Username | All Time Contribution Count | All Commits |
|
||||
| --- | --- | --- |
|
||||
| @casualjim | 88 | <https://github.com/go-openapi/strfmt/commits?author=casualjim> |
|
||||
| @fredbi | 66 | <https://github.com/go-openapi/strfmt/commits?author=fredbi> |
|
||||
| @fredbi | 71 | <https://github.com/go-openapi/strfmt/commits?author=fredbi> |
|
||||
| @youyuanwu | 13 | <https://github.com/go-openapi/strfmt/commits?author=youyuanwu> |
|
||||
| @jlambatl | 9 | <https://github.com/go-openapi/strfmt/commits?author=jlambatl> |
|
||||
| @GlenDC | 5 | <https://github.com/go-openapi/strfmt/commits?author=GlenDC> |
|
||||
|
||||
+8
@@ -1978,6 +1978,14 @@ func (r *Password) DeepCopy() *Password {
|
||||
}
|
||||
|
||||
func isRequestURI(rawurl string) bool {
|
||||
// url.ParseRequestURI assumes the input contains no "#fragment"
|
||||
// (RFC 3986 §3.5). A URI with a fragment and an empty path, such as
|
||||
// "https://host#@frag", is therefore misread as userinfo and rejected
|
||||
// as "invalid userinfo". Strip the fragment first so the absolute
|
||||
// request URI validates, matching url.Parse's RFC 3986 handling.
|
||||
if i := strings.IndexByte(rawurl, '#'); i >= 0 {
|
||||
rawurl = rawurl[:i]
|
||||
}
|
||||
_, err := url.ParseRequestURI(rawurl)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,11 +4,11 @@
|
||||
|
||||
| Total Contributors | Total Contributions |
|
||||
| --- | --- |
|
||||
| 24 | 242 |
|
||||
| 24 | 246 |
|
||||
|
||||
| Username | All Time Contribution Count | All Commits |
|
||||
| --- | --- | --- |
|
||||
| @fredbi | 112 | <https://github.com/go-openapi/swag/commits?author=fredbi> |
|
||||
| @fredbi | 116 | <https://github.com/go-openapi/swag/commits?author=fredbi> |
|
||||
| @casualjim | 98 | <https://github.com/go-openapi/swag/commits?author=casualjim> |
|
||||
| @alexandear | 4 | <https://github.com/go-openapi/swag/commits?author=alexandear> |
|
||||
| @orisano | 3 | <https://github.com/go-openapi/swag/commits?author=orisano> |
|
||||
|
||||
+7
-9
@@ -34,12 +34,9 @@ You may also use it standalone for your projects.
|
||||
|
||||
* **2025-12-19** : new community chat on discord
|
||||
* a new discord community channel is available to be notified of changes and support users
|
||||
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
|
||||
|
||||
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
|
||||
|
||||
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
|
||||
|
||||
## Status
|
||||
|
||||
API is stable.
|
||||
@@ -171,9 +168,9 @@ on top of which it has been built.
|
||||
## Other documentation
|
||||
|
||||
* [All-time contributors](./CONTRIBUTORS.md)
|
||||
* [Contributing guidelines](.github/CONTRIBUTING.md)
|
||||
* [Maintainers documentation](docs/MAINTAINERS.md)
|
||||
* [Code style](docs/STYLE.md)
|
||||
* [Contributing guidelines][contributing-doc-site]
|
||||
* [Maintainers documentation][maintainers-doc-site]
|
||||
* [Code style][style-doc-site]
|
||||
|
||||
## Cutting a new release
|
||||
|
||||
@@ -208,9 +205,6 @@ Maintainers can cut a new release by either:
|
||||
[doc-url]: https://goswagger.io/go-openapi
|
||||
[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/swag
|
||||
[godoc-url]: http://pkg.go.dev/github.com/go-openapi/swag
|
||||
[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
|
||||
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
|
||||
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
|
||||
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
|
||||
[discord-url]: https://discord.gg/FfnFYaC3k5
|
||||
|
||||
@@ -222,3 +216,7 @@ Maintainers can cut a new release by either:
|
||||
[goversion-url]: https://github.com/go-openapi/swag/blob/master/go.mod
|
||||
[top-badge]: https://img.shields.io/github/languages/top/go-openapi/swag
|
||||
[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/swag/latest
|
||||
<!-- Organization docs -->
|
||||
[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html
|
||||
[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html
|
||||
[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html
|
||||
|
||||
+24
@@ -2,4 +2,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package loading provides tools to load a file from http or from a local file system.
|
||||
//
|
||||
// # Security
|
||||
//
|
||||
// By default, the local loader reads any path the process can access, including absolute
|
||||
// paths and "file://" URIs (for example "file:///etc/passwd"). Applications that pass
|
||||
// untrusted input to [LoadFromFileOrHTTP], [JSONDoc] (or to downstream consumers such as
|
||||
// go-openapi/loads) must confine local loading to a trusted directory.
|
||||
//
|
||||
// Use [WithRoot] to do so: it resolves every requested path relative to a chosen directory
|
||||
// and rejects anything that escapes it, including via symlink. It is built on [os.Root]
|
||||
// and is therefore safer than passing an [os.DirFS] to [WithFS], which does not block
|
||||
// symlink escapes.
|
||||
//
|
||||
// Remote loading uses a standard [net/http] client.
|
||||
// By default it follows redirects and performs no destination filtering — exactly like [net/http.DefaultClient].
|
||||
//
|
||||
// A caller-controlled URL may therefore reach internal services or cloud metadata endpoints
|
||||
// (server-side request forgery).
|
||||
//
|
||||
// This package does not, and should not, embed a network policy:
|
||||
// when the URL may derive from untrusted input, supply a restricted client with
|
||||
// [WithHTTPClient] whose transport rejects unwanted destinations at dial time — which also
|
||||
// covers redirects and DNS rebinding.
|
||||
// See the example on [LoadFromFileOrHTTP].
|
||||
package loading
|
||||
|
||||
+32
-4
@@ -17,7 +17,11 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoadFromFileOrHTTP loads the bytes from a file or a remote http server based on the path passed in
|
||||
// LoadFromFileOrHTTP loads the bytes from a file or a remote http server based on the path passed in.
|
||||
//
|
||||
// Security: by default a local path is read with no confinement, so a caller-controlled path
|
||||
// (including a "file://" URI or an absolute path) may read any file the process can access.
|
||||
// When the path may derive from untrusted input, confine local loading with [WithRoot].
|
||||
func LoadFromFileOrHTTP(pth string, opts ...Option) ([]byte, error) {
|
||||
o := optionsWithDefaults(opts)
|
||||
return LoadStrategy(pth, o.ReadFileFunc(), loadHTTPBytes(opts...), opts...)(pth)
|
||||
@@ -54,11 +58,14 @@ func LoadFromFileOrHTTP(pth string, opts ...Option) ([]byte, error) {
|
||||
// - `file:///c:/folder/file` becomes `C:\folder\file`
|
||||
// - `file://c:/folder/file` is tolerated (without leading `/`) and becomes `c:\folder\file`
|
||||
func LoadStrategy(pth string, local, remote func(string) ([]byte, error), opts ...Option) func(string) ([]byte, error) {
|
||||
if strings.HasPrefix(pth, "http") {
|
||||
if hasHTTPScheme(pth) {
|
||||
return remote
|
||||
}
|
||||
o := optionsWithDefaults(opts)
|
||||
_, isEmbedFS := o.fs.(embed.FS)
|
||||
// any loader backed by an fs.FS or an os.Root consumes forward-slash paths on every
|
||||
// platform, so it must not go through the windows-native file:// preprocessing below.
|
||||
isFSBacked := o.fs != nil || o.root != ""
|
||||
|
||||
return func(p string) ([]byte, error) {
|
||||
upth, err := url.PathUnescape(p)
|
||||
@@ -67,14 +74,20 @@ func LoadStrategy(pth string, local, remote func(string) ([]byte, error), opts .
|
||||
}
|
||||
|
||||
cpth, hasPrefix := strings.CutPrefix(upth, "file://")
|
||||
if !hasPrefix || isEmbedFS || runtime.GOOS != "windows" {
|
||||
if !hasPrefix || isFSBacked || runtime.GOOS != "windows" {
|
||||
// crude processing: trim the file:// prefix. This leaves full URIs with a host with a (mostly) unexpected result
|
||||
// regular file path provided: just normalize slashes
|
||||
if isEmbedFS {
|
||||
// on windows, we need to slash the path if FS is an embed FS.
|
||||
// embed.FS always uses "/" as separator, even on windows, and rejects leading "./" or "/".
|
||||
return local(strings.TrimLeft(filepath.ToSlash(cpth), "./")) // remove invalid leading characters for embed FS
|
||||
}
|
||||
|
||||
if isFSBacked {
|
||||
// other fs.FS (e.g. os.DirFS) and os.Root loaders also use "/" on every platform.
|
||||
// Escaping paths (absolute, "..", escaping symlinks) are rejected by the loader, not rewritten here.
|
||||
return local(filepath.ToSlash(cpth))
|
||||
}
|
||||
|
||||
return local(filepath.FromSlash(cpth))
|
||||
}
|
||||
|
||||
@@ -113,6 +126,21 @@ func LoadStrategy(pth string, local, remote func(string) ([]byte, error), opts .
|
||||
}
|
||||
}
|
||||
|
||||
// hasHTTPScheme reports whether pth is an absolute URL with an http or https scheme,
|
||||
// selecting the remote loader. The comparison is case-insensitive, as URL schemes are.
|
||||
//
|
||||
// Requiring the "://" separator (rather than a bare "http" prefix) avoids misrouting a
|
||||
// local file whose name merely starts with "http" (e.g. "httpbin.json") to the remote loader.
|
||||
func hasHTTPScheme(pth string) bool {
|
||||
for _, scheme := range [...]string{"http://", "https://"} {
|
||||
if len(pth) >= len(scheme) && strings.EqualFold(pth[:len(scheme)], scheme) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func loadHTTPBytes(opts ...Option) func(path string) ([]byte, error) {
|
||||
o := optionsWithDefaults(opts)
|
||||
|
||||
|
||||
+46
-1
@@ -4,6 +4,7 @@
|
||||
package loading
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -23,7 +24,8 @@ type (
|
||||
}
|
||||
|
||||
fileOptions struct {
|
||||
fs fs.ReadFileFS
|
||||
fs fs.ReadFileFS
|
||||
root string // when non-empty, local reads are confined to this directory via os.Root
|
||||
}
|
||||
|
||||
options struct {
|
||||
@@ -33,6 +35,20 @@ type (
|
||||
)
|
||||
|
||||
func (fo fileOptions) ReadFileFunc() func(string) ([]byte, error) {
|
||||
if fo.root != "" {
|
||||
root := fo.root
|
||||
|
||||
return func(name string) ([]byte, error) {
|
||||
r, err := os.OpenRoot(root)
|
||||
if err != nil {
|
||||
return nil, errors.Join(err, ErrLoader)
|
||||
}
|
||||
defer func() { _ = r.Close() }()
|
||||
|
||||
return r.ReadFile(name)
|
||||
}
|
||||
}
|
||||
|
||||
if fo.fs == nil {
|
||||
return os.ReadFile
|
||||
}
|
||||
@@ -87,8 +103,15 @@ func WithHTTPClient(client *http.Client) Option {
|
||||
// By default, the file system is the one provided by the os package.
|
||||
//
|
||||
// For example, this may be set to consume from an embedded file system, or a rooted FS.
|
||||
//
|
||||
// WithFS and [WithRoot] are mutually exclusive: the last one applied wins.
|
||||
//
|
||||
// Security note: a file system built from [os.DirFS] confines paths but does NOT protect
|
||||
// against symlinks that escape the root. To load from a directory derived from untrusted
|
||||
// input, prefer [WithRoot], which is symlink-escape resistant.
|
||||
func WithFS(filesystem fs.FS) Option {
|
||||
return func(o *options) {
|
||||
o.root = "" // last-wins vs WithRoot
|
||||
if rfs, ok := filesystem.(fs.ReadFileFS); ok {
|
||||
o.fs = rfs
|
||||
|
||||
@@ -98,6 +121,28 @@ func WithFS(filesystem fs.FS) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRoot confines local file loading to dir.
|
||||
//
|
||||
// Every requested path is resolved relative to dir, and any path that would escape dir —
|
||||
// whether through an absolute path, ".." traversal, or a symlink pointing outside dir — is
|
||||
// rejected. This is built on [os.Root] and is therefore resistant to the symlink escapes
|
||||
// that a plain [os.DirFS] does not prevent.
|
||||
//
|
||||
// WithRoot is the recommended option when loading specs from a location derived from
|
||||
// untrusted input. It applies to local loading only and has no effect on remote
|
||||
// (http/https) loading. WithRoot and [WithFS] are mutually exclusive: the last one applied
|
||||
// wins.
|
||||
//
|
||||
// Note: [os.Root] confines path resolution but does not, by itself, protect against
|
||||
// traversal of mount/bind boundaries, /proc special files, or device files. Point WithRoot
|
||||
// at a directory that holds only the documents you intend to expose.
|
||||
func WithRoot(dir string) Option {
|
||||
return func(o *options) {
|
||||
o.root = dir
|
||||
o.fs = nil // last-wins vs WithFS
|
||||
}
|
||||
}
|
||||
|
||||
type readFileFS struct {
|
||||
fs.FS
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,12 +4,12 @@
|
||||
|
||||
| Total Contributors | Total Contributions |
|
||||
| --- | --- |
|
||||
| 31 | 302 |
|
||||
| 31 | 305 |
|
||||
|
||||
| Username | All Time Contribution Count | All Commits |
|
||||
| --- | --- | --- |
|
||||
| @casualjim | 169 | <https://github.com/go-openapi/validate/commits?author=casualjim> |
|
||||
| @fredbi | 65 | <https://github.com/go-openapi/validate/commits?author=fredbi> |
|
||||
| @fredbi | 68 | <https://github.com/go-openapi/validate/commits?author=fredbi> |
|
||||
| @sttts | 11 | <https://github.com/go-openapi/validate/commits?author=sttts> |
|
||||
| @youyuanwu | 9 | <https://github.com/go-openapi/validate/commits?author=youyuanwu> |
|
||||
| @keramix | 8 | <https://github.com/go-openapi/validate/commits?author=keramix> |
|
||||
|
||||
+7
-9
@@ -18,12 +18,9 @@ A validator for OpenAPI v2 specifications and JSON schema draft 4.
|
||||
|
||||
* **2025-12-19** : new community chat on discord
|
||||
* a new discord community channel is available to be notified of changes and support users
|
||||
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
|
||||
|
||||
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
|
||||
|
||||
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
|
||||
|
||||
## Status
|
||||
|
||||
API is stable.
|
||||
@@ -75,9 +72,9 @@ This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE).
|
||||
## Other documentation
|
||||
|
||||
* [All-time contributors](./CONTRIBUTORS.md)
|
||||
* [Contributing guidelines](.github/CONTRIBUTING.md)
|
||||
* [Maintainers documentation](docs/MAINTAINERS.md)
|
||||
* [Code style](docs/STYLE.md)
|
||||
* [Contributing guidelines][contributing-doc-site]
|
||||
* [Maintainers documentation][maintainers-doc-site]
|
||||
* [Code style][style-doc-site]
|
||||
|
||||
## Cutting a new release
|
||||
|
||||
@@ -108,9 +105,6 @@ Maintainers can cut a new release by either:
|
||||
<!-- Badges: documentation & support -->
|
||||
[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/validate
|
||||
[godoc-url]: http://pkg.go.dev/github.com/go-openapi/validate
|
||||
[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
|
||||
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
|
||||
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
|
||||
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
|
||||
[discord-url]: https://discord.gg/FfnFYaC3k5
|
||||
|
||||
@@ -122,3 +116,7 @@ Maintainers can cut a new release by either:
|
||||
[goversion-url]: https://github.com/go-openapi/validate/blob/master/go.mod
|
||||
[top-badge]: https://img.shields.io/github/languages/top/go-openapi/validate
|
||||
[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/validate/latest
|
||||
<!-- Organization docs -->
|
||||
[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html
|
||||
[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html
|
||||
[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html
|
||||
|
||||
+7
-2
@@ -146,7 +146,8 @@ func (s *SpecValidator) Validate(data any) (*Result, *Result) {
|
||||
errs.Merge(s.validateNonEmptyPathParamNames())
|
||||
|
||||
// errs.Merge(s.validateRefNoSibling()) // warning only
|
||||
errs.Merge(s.validateReferenced()) // warning only
|
||||
errs.Merge(s.validateReferenced()) // warning only
|
||||
errs.Merge(s.validateDubiousRefs()) // warning only
|
||||
|
||||
return errs, warnings
|
||||
}
|
||||
@@ -553,8 +554,12 @@ DEFINITIONS:
|
||||
if schema.Required != nil { // Safeguard
|
||||
for _, pn := range schema.Required {
|
||||
red := s.validateRequiredProperties(pn, d, &schema) //#nosec
|
||||
// NOTE: capture validity before merging: Merge may redeem `red` to the
|
||||
// pool (wantsRedeemOnMerge), after which reading it races with a concurrent
|
||||
// BorrowResult().cleared() in another goroutine sharing the global pool.
|
||||
isValid := red.IsValid()
|
||||
res.Merge(red)
|
||||
if !red.IsValid() && !s.Options.ContinueOnErrors {
|
||||
if !isValid && !s.Options.ContinueOnErrors {
|
||||
break DEFINITIONS // there is an error, let's stop that bleeding
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -177,6 +177,18 @@ const (
|
||||
// UnusedResponseWarning ...
|
||||
UnusedResponseWarning = "response %q is not used anywhere"
|
||||
|
||||
// DubiousAbsoluteRefWarning flags a $ref pointing to an absolute local file location that escapes the
|
||||
// spec's base path. Absolute local references are legitimate when they stay beneath the base path
|
||||
// (flattening/expansion introduces such anchors for cyclical $refs), but an absolute reference that
|
||||
// escapes the base path - or a file:// reference in a spec with no known base - may indicate an
|
||||
// unsafe or adversarial spec.
|
||||
DubiousAbsoluteRefWarning = "$ref %q points to an absolute or local file location that escapes the spec's base path: this may be unsafe with adversarial specs"
|
||||
|
||||
// DubiousMultipleHostsWarning flags a spec whose remote $refs resolve to several distinct hosts.
|
||||
// A single consistent remote host is common and legitimate; references spread across multiple hosts
|
||||
// may indicate an unsafe or adversarial spec.
|
||||
DubiousMultipleHostsWarning = "$ref values point to %d distinct remote hosts (%s): a spec referencing multiple hosts may be unsafe"
|
||||
|
||||
InvalidObject = "expected an object in %q.%s"
|
||||
)
|
||||
|
||||
@@ -404,3 +416,11 @@ func someParametersBrokenMsg(path, method, operationID string) errors.Error {
|
||||
func refShouldNotHaveSiblingsMsg(path, operationID string) errors.Error {
|
||||
return errors.New(errors.CompositeErrorCode, RefShouldNotHaveSiblingsWarning, operationID, path)
|
||||
}
|
||||
|
||||
func dubiousAbsoluteRefMsg(ref string) errors.Error {
|
||||
return errors.New(errors.CompositeErrorCode, DubiousAbsoluteRefWarning, ref)
|
||||
}
|
||||
|
||||
func dubiousMultipleHostsMsg(count int, hosts string) errors.Error {
|
||||
return errors.New(errors.CompositeErrorCode, DubiousMultipleHostsWarning, count, hosts)
|
||||
}
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
)
|
||||
|
||||
// minDistinctHostsToWarn is the number of distinct remote hosts among $refs at or above which
|
||||
// Rule 2 emits a host-spread warning. A single consistent remote host is legitimate.
|
||||
const minDistinctHostsToWarn = 2
|
||||
|
||||
// validateDubiousRefs emits warnings (never errors) when $ref locations match patterns
|
||||
// that may indicate an unsafe or adversarial spec. It inspects refs as authored, on the
|
||||
// UNEXPANDED spec, so it must run before expansion flattens them away.
|
||||
//
|
||||
// Two rules are applied over s.analyzer.AllRefs():
|
||||
//
|
||||
// - Rule 1 (absolute local escape): a $ref pointing to an absolute local file location
|
||||
// (file:// scheme, a Unix absolute path, or a Windows drive path such as C:\) is dubious
|
||||
// UNLESS it stays beneath the spec's base path. Absolute refs beneath the base are
|
||||
// legitimate: flattening/expansion in go-openapi/spec and analysis introduces absolute
|
||||
// anchors to resolve cyclical $refs. Relative and fragment-only refs are always exempt.
|
||||
//
|
||||
// - Rule 2 (host spread): when remote (http/https, or protocol-relative) refs resolve to
|
||||
// two or more distinct hosts, a single aggregate warning lists them. A single consistent
|
||||
// remote host is common and legitimate, so it is not flagged.
|
||||
//
|
||||
// All findings are warnings: they do not affect validity (see Result.IsValid).
|
||||
func (s *SpecValidator) validateDubiousRefs() *Result {
|
||||
res := pools.poolOfResults.BorrowResult()
|
||||
|
||||
baseDir, hasBase := s.localBaseDir()
|
||||
|
||||
remoteHosts := make(map[string]struct{})
|
||||
for _, r := range s.analyzer.AllRefs() {
|
||||
u := r.GetURL()
|
||||
if u == nil { // Safeguard: a valid spec always yields parseable refs
|
||||
continue
|
||||
}
|
||||
|
||||
// Rule 1: absolute local reference escaping the base path.
|
||||
if refPath, isLocalAbs := absoluteLocalRefPath(r, u); isLocalAbs {
|
||||
if !hasBase || !isBeneathBase(refPath, baseDir) {
|
||||
res.AddWarnings(dubiousAbsoluteRefMsg(r.String()))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Rule 2: gather remote hosts (http/https and protocol-relative //host/...).
|
||||
if host := remoteRefHost(u); host != "" {
|
||||
remoteHosts[host] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if len(remoteHosts) >= minDistinctHostsToWarn {
|
||||
hosts := make([]string, 0, len(remoteHosts))
|
||||
for h := range remoteHosts {
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
sort.Strings(hosts)
|
||||
res.AddWarnings(dubiousMultipleHostsMsg(len(hosts), strings.Join(hosts, ", ")))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// absoluteLocalRefPath reports whether r is an absolute LOCAL file reference and, if so,
|
||||
// returns the cleaned path it points to (without scheme/fragment, drive letter lower-cased).
|
||||
//
|
||||
// Classification order matters (see the empirical jsonreference flag behavior):
|
||||
// - file:// scheme is local, including UNC file://host/share (inherently dubious).
|
||||
// - a non-empty Host with no file scheme means remote (http/https or protocol-relative
|
||||
// //host/path) - NOT local; handled by Rule 2. This must be checked before the Unix
|
||||
// branch, because protocol-relative refs also set HasFullFilePath.
|
||||
// - len(u.Scheme) == 1 is a Windows drive path (C:\ or C:/), whose drive+path land in
|
||||
// Scheme/Opaque/Path rather than Path. Checked before the Unix branch because C:/x also
|
||||
// sets HasFullFilePath, and reconstructed from the authored ref string to keep the drive.
|
||||
// - !r.HasFullURL && r.HasFullFilePath is a plain Unix absolute path (/abs/models.json).
|
||||
//
|
||||
// Relative (./x.json) and fragment-only (#/definitions/X) refs return false.
|
||||
func absoluteLocalRefPath(r spec.Ref, u *url.URL) (string, bool) {
|
||||
switch {
|
||||
case r.HasFileScheme:
|
||||
return fileRefPath(u), true
|
||||
case u.Host != "":
|
||||
// Remote (http/https) or protocol-relative //host/path: handled by Rule 2.
|
||||
return "", false
|
||||
case len(u.Scheme) == 1:
|
||||
// Windows drive letter: reconstruct from the authored ref string.
|
||||
return cleanRefPath(r.String()), true
|
||||
case !r.HasFullURL && r.HasFullFilePath:
|
||||
return cleanRefPath(u.Path), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// remoteRefHost returns the host of a remote reference (http/https), or of a protocol-relative
|
||||
// reference (//host/path). It returns "" for local and fragment-only refs. file:// hosts (UNC)
|
||||
// are deliberately excluded: those are handled as local-absolute refs by Rule 1.
|
||||
func remoteRefHost(u *url.URL) string {
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
return u.Host
|
||||
case "":
|
||||
// Protocol-relative //host/path: empty scheme but a host is present.
|
||||
return u.Host
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// localBaseDir returns the directory of the spec file, slash-normalized, when the spec was
|
||||
// loaded from a local path. It returns ok=false when the base is unknown (in-memory spec) or
|
||||
// remote (http/https), in which case absolute-local refs cannot be proven beneath a base and
|
||||
// are treated as dubious.
|
||||
func (s *SpecValidator) localBaseDir() (string, bool) {
|
||||
specPath := s.spec.SpecFilePath()
|
||||
if specPath == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Strip a file:// scheme if present; reject remote bases.
|
||||
if u, err := url.Parse(specPath); err == nil && u.Scheme != "" {
|
||||
switch {
|
||||
case u.Scheme == "file":
|
||||
specPath = u.Path
|
||||
case len(u.Scheme) == 1: // Windows drive letter, treat as local
|
||||
// keep specPath as-is (authored path)
|
||||
default: // http, https, ... : no local base
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
return path.Dir(cleanRefPath(specPath)), true
|
||||
}
|
||||
|
||||
// isBeneathBase reports whether the cleaned target path is located within baseDir, i.e. it does
|
||||
// not escape baseDir via "..". Comparison is purely lexical on cleaned paths, which is sufficient
|
||||
// (and cross-platform safe) for a non-fatal warning. Both sides are expected to already be
|
||||
// cleanRefPath-normalized (slashes, drive-letter case).
|
||||
func isBeneathBase(target, baseDir string) bool {
|
||||
if baseDir == "" {
|
||||
return false
|
||||
}
|
||||
if target == baseDir {
|
||||
return true
|
||||
}
|
||||
if !strings.HasSuffix(baseDir, "/") {
|
||||
baseDir += "/"
|
||||
}
|
||||
return strings.HasPrefix(target, baseDir)
|
||||
}
|
||||
|
||||
// fileRefPath extracts the local path a file:// reference points to, accounting for the way
|
||||
// Windows file URLs parse:
|
||||
// - file:///abs/x -> /abs/x (empty host)
|
||||
// - file:///C:/dir/x -> /c:/dir/x (empty host; drive sits in the path)
|
||||
// - file://D:/a/x -> d:/a/x (drive letter lands in Host, rejoin it)
|
||||
// - file://host/share -> /host/share/x (real UNC host kept visible so it cannot match a
|
||||
// local base and stays flagged as dubious)
|
||||
func fileRefPath(u *url.URL) string {
|
||||
switch {
|
||||
case u.Host == "":
|
||||
return cleanRefPath(u.Path)
|
||||
case isDriveHost(u.Host):
|
||||
// Windows path authored as file://D:/... : the drive landed in Host (e.g. "d:").
|
||||
return cleanRefPath(u.Host + u.Path)
|
||||
default:
|
||||
// Real remote/UNC host: keep it in the path so it never matches a local base.
|
||||
return cleanRefPath("//" + u.Host + u.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// isDriveHost reports whether a URL host is actually a Windows drive letter (e.g. "d:"), which
|
||||
// happens when a Windows path is authored as a two-slash file URL: file://D:/path.
|
||||
func isDriveHost(host string) bool {
|
||||
h := strings.TrimSuffix(host, ":")
|
||||
if len(h) != 1 {
|
||||
return false
|
||||
}
|
||||
c := h[0]
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
// cleanRefPath normalizes a ref or base path for lexical comparison: backslashes to forward
|
||||
// slashes, path.Clean, and a lower-cased leading Windows drive letter (matching the behavior of
|
||||
// go-openapi/spec's normalizer). Plain Unix paths are unaffected, preserving case-sensitivity.
|
||||
func cleanRefPath(p string) string {
|
||||
p = path.Clean(strings.ReplaceAll(p, `\`, `/`))
|
||||
switch {
|
||||
case len(p) >= 2 && p[1] == ':':
|
||||
// drive-letter form: C:/dir -> c:/dir
|
||||
p = strings.ToLower(p[:1]) + p[1:]
|
||||
case len(p) >= 3 && p[0] == '/' && p[2] == ':':
|
||||
// slash-prefixed drive form from canonical file:// URLs: /C:/dir -> c:/dir.
|
||||
// The leading slash is dropped so this matches the base path derived from
|
||||
// SpecFilePath (which has no leading slash), and the bare-drive form.
|
||||
p = strings.ToLower(p[1:2]) + p[2:]
|
||||
}
|
||||
return p
|
||||
}
|
||||
Reference in New Issue
Block a user