snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
24
oryx/urlx/copy.go
Normal file
24
oryx/urlx/copy.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package urlx
|
||||
|
||||
import "net/url"
|
||||
|
||||
// Copy returns a copy of the input url.
|
||||
func Copy(src *url.URL) *url.URL {
|
||||
var out = new(url.URL)
|
||||
*out = *src
|
||||
return out
|
||||
}
|
||||
|
||||
// CopyWithQuery returns a copy of the input url with the given query parameters
|
||||
func CopyWithQuery(src *url.URL, query url.Values) *url.URL {
|
||||
out := Copy(src)
|
||||
q := out.Query()
|
||||
for k := range query {
|
||||
q.Set(k, query.Get(k))
|
||||
}
|
||||
out.RawQuery = q.Encode()
|
||||
return out
|
||||
}
|
||||
139
oryx/urlx/extract.go
Normal file
139
oryx/urlx/extract.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package urlx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// hostCache caches DNS lookup results for hostnames to avoid repeated lookups.
|
||||
// The cache is thread-safe and stores true/false whether a hostname resolves to public IPs.
|
||||
type hostCache struct {
|
||||
mu sync.RWMutex
|
||||
cache map[string]bool
|
||||
}
|
||||
|
||||
// get retrieves a cached value for a hostname. Returns value and whether it was found.
|
||||
func (hc *hostCache) get(hostname string) (bool, bool) {
|
||||
hc.mu.RLock()
|
||||
defer hc.mu.RUnlock()
|
||||
isPublic, found := hc.cache[hostname]
|
||||
return isPublic, found
|
||||
}
|
||||
|
||||
// set stores the lookup result for a hostname.
|
||||
func (hc *hostCache) set(hostname string, isPublic bool) {
|
||||
hc.mu.Lock()
|
||||
defer hc.mu.Unlock()
|
||||
hc.cache[hostname] = isPublic
|
||||
}
|
||||
|
||||
// localCache lives for the lifetime of the main process. The cache
|
||||
// size is not expected to grow more than a few hundred bytes.
|
||||
var localCache = &hostCache{
|
||||
cache: make(map[string]bool),
|
||||
}
|
||||
|
||||
// ExtractPublicAddress iterates over parameters and extracts the first public
|
||||
// address found. Parameter values are assumed to be in priority order. Returns
|
||||
// an empty string if only private addresses are available.
|
||||
func ExtractPublicAddress(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value == "" || value == "*" {
|
||||
continue
|
||||
}
|
||||
host := value
|
||||
|
||||
// parse URL addresses
|
||||
if u, err := url.Parse(value); err == nil && len(u.Host) > 1 {
|
||||
host = removeWildcardsFromHostname(u.Host)
|
||||
}
|
||||
|
||||
// strip port on both URL and non-URL addresses
|
||||
hostname, _, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
hostname = host
|
||||
}
|
||||
|
||||
// for IP addresses
|
||||
if ip := net.ParseIP(hostname); ip != nil {
|
||||
if !isPrivateIP(ip) {
|
||||
return host
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// for hostnames, first check cache
|
||||
if isPublic, found := localCache.get(hostname); found {
|
||||
if isPublic {
|
||||
return host
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// otherwise, perform DNS lookup & cache result
|
||||
isPublic := isPublicHostname(hostname)
|
||||
localCache.set(hostname, isPublic)
|
||||
if isPublic {
|
||||
return host
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// isPrivateIP checks if an IP address is private (RFC 1918/4193).
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
return ip.IsPrivate() ||
|
||||
ip.IsLoopback() ||
|
||||
ip.IsLinkLocalUnicast() ||
|
||||
ip.IsLinkLocalMulticast() ||
|
||||
ip.IsUnspecified() // 0.0.0.0 or ::
|
||||
}
|
||||
|
||||
// isPublicHostname performs DNS lookup to determine if hostname resolves to public IPs.
|
||||
// Returns true if at least one resolved IP is public, false if all are private or lookup fails.
|
||||
func isPublicHostname(hostname string) bool {
|
||||
// avoid DNS lookup if localhost
|
||||
lower := strings.ToLower(hostname)
|
||||
if lower == "localhost" {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeoutCause(context.Background(), 2*time.Second, errors.Errorf("failed to resolve DNS for %s within 2s", hostname))
|
||||
defer cancel()
|
||||
|
||||
ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if !isPrivateIP(ip.IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// removeWildcardsFromHostname removes wildcard segments from a hostname string
|
||||
// by splitting on dots and filtering out asterisk-only segments.
|
||||
func removeWildcardsFromHostname(hostname string) string {
|
||||
sep := strings.Split(hostname, ".")
|
||||
clean := make([]string, 0, len(sep))
|
||||
for _, s := range sep {
|
||||
if s != "*" && s != "" {
|
||||
clean = append(clean, s)
|
||||
}
|
||||
}
|
||||
return strings.Join(clean, ".")
|
||||
}
|
||||
52
oryx/urlx/join.go
Normal file
52
oryx/urlx/join.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package urlx
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/ory/x/cmdx"
|
||||
)
|
||||
|
||||
// MustJoin joins the paths of two URLs. Fatals if first is not a DSN.
|
||||
func MustJoin(first string, parts ...string) string {
|
||||
u, err := url.Parse(first)
|
||||
if err != nil {
|
||||
cmdx.Fatalf("Unable to parse %s: %s", first, err)
|
||||
}
|
||||
return AppendPaths(u, parts...).String()
|
||||
}
|
||||
|
||||
// AppendPaths appends the provided paths to the url.
|
||||
// Paths are intentionally *not* URL encoded.
|
||||
// The caller is responsible for url encoding, possibly selectively, the required path components with `url.PathEscape`.
|
||||
func AppendPaths(u *url.URL, paths ...string) (ep *url.URL) {
|
||||
ep = Copy(u)
|
||||
if len(paths) == 0 {
|
||||
return ep
|
||||
}
|
||||
|
||||
ep.Path = path.Join(append([]string{ep.Path}, paths...)...)
|
||||
|
||||
last := paths[len(paths)-1]
|
||||
if last != "" && last[len(last)-1] == '/' {
|
||||
ep.Path = ep.Path + "/"
|
||||
}
|
||||
|
||||
return ep
|
||||
}
|
||||
|
||||
// SetQuery appends the provided url values to the DSN's query string.
|
||||
func SetQuery(u *url.URL, query url.Values) (ep *url.URL) {
|
||||
ep = Copy(u)
|
||||
q := ep.Query()
|
||||
|
||||
for k := range query {
|
||||
q.Set(k, query.Get(k))
|
||||
}
|
||||
|
||||
ep.RawQuery = q.Encode()
|
||||
return ep
|
||||
}
|
||||
119
oryx/urlx/parse.go
Normal file
119
oryx/urlx/parse.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package urlx
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/x/logrusx"
|
||||
)
|
||||
|
||||
// winPathRegex is a regex for [DRIVE-LETTER]:
|
||||
var winPathRegex = regexp.MustCompile("^[A-Za-z]:.*")
|
||||
|
||||
// Parse parses rawURL into a URL structure with special handling for file:// URLs
|
||||
//
|
||||
// File URLs with relative paths (file://../file, ../file) will be returned as a
|
||||
// url.URL object without the Scheme set to "file". This is because the file
|
||||
// scheme does not support relative paths. Make sure to check for
|
||||
// both "file" or "" (an empty string) in URL.Scheme if you are looking for
|
||||
// a file path.
|
||||
//
|
||||
// Use the companion function GetURLFilePath() to get a file path suitable
|
||||
// for the current operating system.
|
||||
func Parse(rawURL string) (*url.URL, error) {
|
||||
lcRawURL := strings.ToLower(rawURL)
|
||||
if strings.HasPrefix(lcRawURL, "file:///") {
|
||||
return url.Parse(rawURL)
|
||||
}
|
||||
|
||||
// Normally the first part after file:// is a hostname, but since
|
||||
// this is often misused we interpret the URL like a normal path
|
||||
// by removing the "file://" from the beginning (if it exists)
|
||||
rawURL = trimPrefixIC(rawURL, "file://")
|
||||
|
||||
if winPathRegex.MatchString(rawURL) {
|
||||
// Windows path
|
||||
return url.Parse("file:///" + rawURL)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(lcRawURL, "\\\\") {
|
||||
// Windows UNC path
|
||||
// We extract the hostname and create an appropriate file:// URL
|
||||
// based on the hostname and the path
|
||||
host, path := extractUNCPathParts(rawURL)
|
||||
// It is safe to replace the \ with / here because this is POSIX style path
|
||||
return url.Parse("file://" + host + strings.ReplaceAll(path, "\\", "/"))
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Since go1.19:
|
||||
//
|
||||
// > The URL type now distinguishes between URLs with no authority and URLs with an empty authority.
|
||||
// > For example, http:///path has an empty authority (host), while http:/path has none.
|
||||
//
|
||||
// See https://golang.org/doc/go1.19#net/url for more details.
|
||||
parsed.OmitHost = false
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// ParseOrPanic parses a url or panics.
|
||||
func ParseOrPanic(in string) *url.URL {
|
||||
out, err := url.Parse(in)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseOrFatal parses a url or fatals.
|
||||
func ParseOrFatal(l *logrusx.Logger, in string) *url.URL {
|
||||
out, err := url.Parse(in)
|
||||
if err != nil {
|
||||
l.WithError(err).Fatalf("Unable to parse url: %s", in)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseRequestURIOrPanic parses a request uri or panics.
|
||||
func ParseRequestURIOrPanic(in string) *url.URL {
|
||||
out, err := url.ParseRequestURI(in)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseRequestURIOrFatal parses a request uri or fatals.
|
||||
func ParseRequestURIOrFatal(l *logrusx.Logger, in string) *url.URL {
|
||||
out, err := url.ParseRequestURI(in)
|
||||
if err != nil {
|
||||
l.WithError(err).Fatalf("Unable to parse url: %s", in)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func extractUNCPathParts(uncPath string) (host, path string) {
|
||||
parts := strings.Split(strings.TrimPrefix(uncPath, "\\\\"), "\\")
|
||||
host = parts[0]
|
||||
if len(parts) > 0 {
|
||||
path = "\\" + strings.Join(parts[1:], "\\")
|
||||
}
|
||||
return host, path
|
||||
}
|
||||
|
||||
// trimPrefixIC returns s without the provided leading prefix string using
|
||||
// case insensitive matching.
|
||||
// If s doesn't start with prefix, s is returned unchanged.
|
||||
func trimPrefixIC(s, prefix string) string {
|
||||
if strings.HasPrefix(strings.ToLower(s), prefix) {
|
||||
return s[len(prefix):]
|
||||
}
|
||||
return s
|
||||
}
|
||||
19
oryx/urlx/path.go
Normal file
19
oryx/urlx/path.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package urlx
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// GetURLFilePath returns the path of a URL that is compatible with the runtime os filesystem
|
||||
func GetURLFilePath(u *url.URL) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
return u.Path
|
||||
}
|
||||
37
oryx/urlx/path_windows.go
Normal file
37
oryx/urlx/path_windows.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package urlx
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetURLFilePath returns the path of a URL that is compatible with the runtime os filesystem
|
||||
func GetURLFilePath(u *url.URL) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
if !(u.Scheme == "file" || u.Scheme == "") {
|
||||
return u.Path
|
||||
}
|
||||
|
||||
fPath := u.Path
|
||||
if u.Host != "" {
|
||||
// Make UNC Path
|
||||
fPath = "\\\\" + u.Host + filepath.FromSlash(fPath)
|
||||
return fPath
|
||||
}
|
||||
fPathTrimmed := strings.TrimLeft(fPath, "/")
|
||||
if winPathRegex.MatchString(fPathTrimmed) {
|
||||
// On Windows we should remove the initial path separator in case this
|
||||
// is a normal path (for example: "\c:\" -> "c:\"")
|
||||
fPath = fPathTrimmed
|
||||
}
|
||||
return filepath.FromSlash(fPath)
|
||||
}
|
||||
Reference in New Issue
Block a user