photoprism/pkg/http/scheme/url.go

130 lines
3.6 KiB
Go

package scheme
import (
"net/url"
"strings"
"sync"
)
// NormalizeBaseURLMaxLen caps the input length eligible for caching; longer
// strings still normalize correctly but bypass the cache so a single oversized
// input cannot bloat the cache map.
const NormalizeBaseURLMaxLen = 2048
// NormalizeBaseURLCacheEntries bounds the in-memory cache. In practice only a
// handful of distinct base URLs (SiteUrl, CdnUrl, possibly an empty string)
// are normalized over the process lifetime; the cap is a safety net so a
// runaway caller cannot grow the map without limit.
const NormalizeBaseURLCacheEntries = 128
var (
normalizeBaseURLCache = make(map[string]string, NormalizeBaseURLCacheEntries)
normalizeBaseURLCacheMu sync.RWMutex
)
// NormalizeBaseURL returns a base URL with a trailing slash if s is not empty
// or does not contain only whitespace. The default port, query strings,
// and fragments are omitted, but Userinfo is preserved.
// If parsing fails, NormalizeBaseURL returns s with a trailing slash appended.
//
// Results are cached by raw input string. The output is a pure function of the
// input, so cache entries never need to be invalidated; the only bounds are
// the input length cap and the maximum number of cached entries.
func NormalizeBaseURL(s string) string {
if len(s) > NormalizeBaseURLMaxLen {
return normalizeBaseURL(s)
}
normalizeBaseURLCacheMu.RLock()
if out, ok := normalizeBaseURLCache[s]; ok {
normalizeBaseURLCacheMu.RUnlock()
return out
}
normalizeBaseURLCacheMu.RUnlock()
out := normalizeBaseURL(s)
normalizeBaseURLCacheMu.Lock()
if len(normalizeBaseURLCache) < NormalizeBaseURLCacheEntries {
normalizeBaseURLCache[s] = out
}
normalizeBaseURLCacheMu.Unlock()
return out
}
// normalizeBaseURL implements the uncached normalization used by NormalizeBaseURL.
func normalizeBaseURL(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
u, err := url.Parse(s)
if err != nil {
return strings.TrimRight(s, "/") + "/"
}
StripDefaultPort(u)
u.RawQuery = ""
u.ForceQuery = false
u.Fragment = ""
u.RawFragment = ""
u.Path = strings.TrimRight(u.Path, "/") + "/"
return u.String()
}
// OriginURL returns the scheme://host/ origin of s (trailing slash; no path,
// query, fragment, or userinfo). A non-default port is preserved; the scheme's
// default port is stripped so the result matches a NormalizeBaseURL'd issuer.
// Returns "" when s is empty or has no scheme/host.
func OriginURL(s string) string {
u, err := url.Parse(strings.TrimSpace(s))
if err != nil || u.Scheme == "" || u.Host == "" {
return ""
}
StripDefaultPort(u)
return u.Scheme + "://" + u.Host + "/"
}
// ResolveAdvertiseURL returns the normalized base URL the Portal should
// use to reach a PhotoPrism instance, using siteURL's path as the source
// of truth. The result always carries a trailing slash so callers can
// concatenate an API path directly (e.g. `result + "api/v1/users/" + uid`).
func ResolveAdvertiseURL(advertiseURL, siteURL string) string {
advertiseURL = NormalizeBaseURL(advertiseURL)
siteURL = NormalizeBaseURL(siteURL)
if advertiseURL == "" && siteURL == "" {
return ""
}
if advertiseURL == "" || advertiseURL == siteURL {
return siteURL
}
// If paths diverge, use the SiteURL's path as the authoritative source.
var sitePath string
if siteURL != "" {
if s, err := url.Parse(siteURL); err == nil {
sitePath = strings.TrimRight(s.Path, "/")
}
}
a, err := url.Parse(advertiseURL)
if err != nil || a.Host == "" {
return advertiseURL
}
if sitePath != "" {
a.Path = sitePath + "/"
a.RawPath = ""
}
return a.String()
}