2020-05-05 07:53:42 +00:00
package main
import (
2023-09-20 13:04:52 +00:00
"bytes"
2024-03-06 23:02:13 +00:00
"context"
2021-04-02 19:14:53 +00:00
"encoding/base64"
2020-05-05 07:53:42 +00:00
"flag"
"fmt"
2024-03-06 23:02:13 +00:00
"math"
"net"
2023-11-03 11:04:17 +00:00
"net/http"
2020-05-05 07:53:42 +00:00
"net/url"
2021-05-21 13:34:03 +00:00
"os"
2021-03-05 16:21:11 +00:00
"regexp"
2024-01-21 02:40:52 +00:00
"sort"
2020-05-05 07:53:42 +00:00
"strings"
"sync"
"sync/atomic"
2023-03-23 08:34:12 +00:00
"time"
2020-05-05 07:53:42 +00:00
2023-11-03 11:04:17 +00:00
"github.com/VictoriaMetrics/metrics"
2024-01-21 02:40:52 +00:00
"github.com/cespare/xxhash/v2"
2023-11-03 11:04:17 +00:00
"gopkg.in/yaml.v2"
2020-08-13 13:43:55 +00:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
2023-02-11 08:27:40 +00:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
2023-12-08 21:27:53 +00:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
2024-01-21 19:58:26 +00:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs/fscore"
2020-05-05 07:53:42 +00:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
)
var (
2021-12-02 22:08:42 +00:00
authConfigPath = flag . String ( "auth.config" , "" , "Path to auth config. It can point either to local file or to http url. " +
"See https://docs.victoriametrics.com/vmauth.html for details on the format of this auth config" )
2023-03-23 08:34:12 +00:00
configCheckInterval = flag . Duration ( "configCheckInterval" , 0 , "interval for config file re-read. " +
"Zero value disables config re-reading. By default, refreshing is disabled, send SIGHUP for config refresh." )
2023-12-08 21:27:53 +00:00
defaultRetryStatusCodes = flagutil . NewArrayInt ( "retryStatusCodes" , 0 , "Comma-separated list of default HTTP response status codes when vmauth re-tries the request on other backends. " +
"See https://docs.victoriametrics.com/vmauth.html#load-balancing for details" )
defaultLoadBalancingPolicy = flag . String ( "loadBalancingPolicy" , "least_loaded" , "The default load balancing policy to use for backend urls specified inside url_prefix section. " +
2024-03-06 23:02:13 +00:00
"Supported policies: least_loaded, first_available. See https://docs.victoriametrics.com/vmauth.html#load-balancing" )
discoverBackendIPsGlobal = flag . Bool ( "discoverBackendIPs" , false , "Whether to discover backend IPs via periodic DNS queries to hostnames specified in url_prefix. " +
"This may be useful when url_prefix points to a hostname with dynamically scaled instances behind it. See https://docs.victoriametrics.com/vmauth.html#discovering-backend-ips" )
discoverBackendIPsInterval = flag . Duration ( "discoverBackendIPsInterval" , 10 * time . Second , "The interval for re-discovering backend IPs if -discoverBackendIPs command-line flag is set. " +
"Too low value may lead to DNS errors" )
2024-04-02 16:26:49 +00:00
httpAuthHeader = flagutil . NewArrayString ( "httpAuthHeader" , "HTTP request header to use for obtaining authorization tokens. By default auth tokens are read from Authorization request header" )
2020-05-05 07:53:42 +00:00
)
// AuthConfig represents auth config.
type AuthConfig struct {
2023-04-24 12:57:13 +00:00
Users [ ] UserInfo ` yaml:"users,omitempty" `
UnauthorizedUser * UserInfo ` yaml:"unauthorized_user,omitempty" `
2024-02-13 18:49:17 +00:00
// ms holds all the metrics for the given AuthConfig
ms * metrics . Set
2020-05-05 07:53:42 +00:00
}
// UserInfo is user information read from authConfigPath
type UserInfo struct {
2024-03-06 18:05:55 +00:00
Name string ` yaml:"name,omitempty" `
BearerToken string ` yaml:"bearer_token,omitempty" `
2024-04-02 18:15:44 +00:00
AuthToken string ` yaml:"auth_token,omitempty" `
2024-03-06 18:05:55 +00:00
Username string ` yaml:"username,omitempty" `
Password string ` yaml:"password,omitempty" `
2023-11-13 21:30:39 +00:00
URLPrefix * URLPrefix ` yaml:"url_prefix,omitempty" `
2024-03-06 23:02:13 +00:00
DiscoverBackendIPs * bool ` yaml:"discover_backend_ips,omitempty" `
2023-11-13 21:30:39 +00:00
URLMaps [ ] URLMap ` yaml:"url_map,omitempty" `
HeadersConf HeadersConf ` yaml:",inline" `
MaxConcurrentRequests int ` yaml:"max_concurrent_requests,omitempty" `
DefaultURL * URLPrefix ` yaml:"default_url,omitempty" `
RetryStatusCodes [ ] int ` yaml:"retry_status_codes,omitempty" `
2023-12-08 21:27:53 +00:00
LoadBalancingPolicy string ` yaml:"load_balancing_policy,omitempty" `
2023-12-13 23:04:46 +00:00
DropSrcPathPrefixParts * int ` yaml:"drop_src_path_prefix_parts,omitempty" `
2023-11-13 21:30:39 +00:00
TLSInsecureSkipVerify * bool ` yaml:"tls_insecure_skip_verify,omitempty" `
TLSCAFile string ` yaml:"tls_ca_file,omitempty" `
2023-02-10 04:03:01 +00:00
2024-01-21 02:40:52 +00:00
MetricLabels map [ string ] string ` yaml:"metric_labels,omitempty" `
2023-02-10 04:03:01 +00:00
concurrencyLimitCh chan struct { }
concurrencyLimitReached * metrics . Counter
2020-05-05 07:53:42 +00:00
2023-11-03 11:04:17 +00:00
httpTransport * http . Transport
2023-06-27 18:15:17 +00:00
requests * metrics . Counter
2024-01-21 02:40:52 +00:00
backendErrors * metrics . Counter
2023-06-27 18:15:17 +00:00
requestsDuration * metrics . Summary
2020-05-05 07:53:42 +00:00
}
2023-08-31 12:26:51 +00:00
// HeadersConf represents config for request and response headers.
type HeadersConf struct {
RequestHeaders [ ] Header ` yaml:"headers,omitempty" `
ResponseHeaders [ ] Header ` yaml:"response_headers,omitempty" `
}
2023-02-10 04:03:01 +00:00
func ( ui * UserInfo ) beginConcurrencyLimit ( ) error {
select {
case ui . concurrencyLimitCh <- struct { } { } :
return nil
default :
ui . concurrencyLimitReached . Inc ( )
2023-02-11 08:27:40 +00:00
return fmt . Errorf ( "cannot handle more than %d concurrent requests from user %s" , ui . getMaxConcurrentRequests ( ) , ui . name ( ) )
2023-02-10 04:03:01 +00:00
}
}
func ( ui * UserInfo ) endConcurrencyLimit ( ) {
2023-02-11 05:57:49 +00:00
<- ui . concurrencyLimitCh
}
func ( ui * UserInfo ) getMaxConcurrentRequests ( ) int {
mcr := ui . MaxConcurrentRequests
2023-02-12 04:51:39 +00:00
if mcr <= 0 {
2023-02-11 05:57:49 +00:00
mcr = * maxConcurrentPerUserRequests
2023-02-10 04:03:01 +00:00
}
2023-02-11 05:57:49 +00:00
return mcr
2023-02-10 04:03:01 +00:00
}
2021-10-22 16:08:06 +00:00
// Header is `Name: Value` http header, which must be added to the proxied request.
type Header struct {
Name string
Value string
2024-03-06 23:02:13 +00:00
sOriginal string
2021-10-22 16:08:06 +00:00
}
// UnmarshalYAML unmarshals h from f.
func ( h * Header ) UnmarshalYAML ( f func ( interface { } ) error ) error {
var s string
if err := f ( & s ) ; err != nil {
return err
}
2024-03-06 23:02:13 +00:00
h . sOriginal = s
2021-10-22 16:08:06 +00:00
n := strings . IndexByte ( s , ':' )
if n < 0 {
return fmt . Errorf ( "missing speparator char ':' between Name and Value in the header %q; expected format - 'Name: Value'" , s )
}
h . Name = strings . TrimSpace ( s [ : n ] )
h . Value = strings . TrimSpace ( s [ n + 1 : ] )
return nil
}
// MarshalYAML marshals h to yaml.
func ( h * Header ) MarshalYAML ( ) ( interface { } , error ) {
2024-03-06 23:02:13 +00:00
return h . sOriginal , nil
2021-10-22 16:08:06 +00:00
}
2021-02-11 10:40:59 +00:00
// URLMap is a mapping from source paths to target urls.
type URLMap struct {
2024-03-06 19:56:32 +00:00
// SrcPaths is an optional list of regular expressions, which must match the request path.
2024-03-06 18:52:23 +00:00
SrcPaths [ ] * Regex ` yaml:"src_paths,omitempty" `
// SrcHosts is an optional list of regular expressions, which must match the request hostname.
2023-12-13 22:46:36 +00:00
SrcHosts [ ] * Regex ` yaml:"src_hosts,omitempty" `
2024-03-06 18:52:23 +00:00
// SrcQueryArgs is an optional list of query args, which must match request URL query args.
SrcQueryArgs [ ] QueryArg ` yaml:"src_query_args,omitempty" `
2023-12-13 22:46:36 +00:00
2024-03-06 19:56:32 +00:00
// SrcHeaders is an optional list of headers, which must match request headers.
SrcHeaders [ ] Header ` yaml:"src_headers,omitempty" `
2023-12-13 22:46:36 +00:00
// UrlPrefix contains backend url prefixes for the proxied request url.
URLPrefix * URLPrefix ` yaml:"url_prefix,omitempty" `
2024-03-06 23:02:13 +00:00
// DiscoverBackendIPs instructs discovering URLPrefix backend IPs via DNS.
DiscoverBackendIPs * bool ` yaml:"discover_backend_ips,omitempty" `
2023-12-13 22:46:36 +00:00
// HeadersConf is the config for augumenting request and response headers.
HeadersConf HeadersConf ` yaml:",inline" `
// RetryStatusCodes is the list of response status codes used for retrying requests.
RetryStatusCodes [ ] int ` yaml:"retry_status_codes,omitempty" `
// LoadBalancingPolicy is load balancing policy among UrlPrefix backends.
LoadBalancingPolicy string ` yaml:"load_balancing_policy,omitempty" `
// DropSrcPathPrefixParts is the number of `/`-delimited request path prefix parts to drop before proxying the request to backend.
2023-12-13 23:04:46 +00:00
DropSrcPathPrefixParts * int ` yaml:"drop_src_path_prefix_parts,omitempty" `
2021-03-05 16:21:11 +00:00
}
2023-12-13 22:46:36 +00:00
// Regex represents a regex
type Regex struct {
2024-03-06 19:19:43 +00:00
re * regexp . Regexp
2021-03-05 16:21:11 +00:00
sOriginal string
}
2024-03-06 19:19:43 +00:00
// QueryArg represents HTTP query arg
2024-03-06 18:52:23 +00:00
type QueryArg struct {
2024-03-06 19:56:32 +00:00
Name string
Value string
2024-03-06 19:19:43 +00:00
sOriginal string
}
// UnmarshalYAML unmarshals up from yaml.
func ( qa * QueryArg ) UnmarshalYAML ( f func ( interface { } ) error ) error {
2024-03-06 23:02:13 +00:00
var s string
if err := f ( & s ) ; err != nil {
2024-03-06 19:19:43 +00:00
return err
}
2024-03-06 23:02:13 +00:00
qa . sOriginal = s
2024-03-06 19:19:43 +00:00
2024-03-06 23:02:13 +00:00
n := strings . IndexByte ( s , '=' )
2024-03-06 19:19:43 +00:00
if n >= 0 {
2024-03-06 23:02:13 +00:00
qa . Name = s [ : n ]
qa . Value = s [ n + 1 : ]
2024-03-06 19:19:43 +00:00
}
return nil
}
// MarshalYAML marshals up to yaml.
func ( qa * QueryArg ) MarshalYAML ( ) ( interface { } , error ) {
return qa . sOriginal , nil
2024-03-06 18:52:23 +00:00
}
2023-02-13 12:27:13 +00:00
// URLPrefix represents passed `url_prefix`
2021-05-28 22:00:23 +00:00
type URLPrefix struct {
2023-12-08 21:27:53 +00:00
// requests are re-tried on other backend urls for these http response status codes
retryStatusCodes [ ] int
// load balancing policy used
loadBalancingPolicy string
2023-12-13 23:04:46 +00:00
2024-03-06 23:02:13 +00:00
// how many request path prefix parts to drop before routing the request to backendURL
2023-12-13 23:04:46 +00:00
dropSrcPathPrefixParts int
2024-03-06 23:02:13 +00:00
// busOriginal contains the original list of backends specified in yaml config.
busOriginal [ ] * url . URL
// n is an atomic counter, which is used for balancing load among available backends.
n atomic . Uint32
// the list of backend urls
//
// the list can be dynamically updated if `discover_backend_ips` option is set.
bus atomic . Pointer [ [ ] * backendURL ]
// if this option is set, then backend ips for busOriginal are periodically re-discovered and put to bus.
discoverBackendIPs bool
// The next deadline for DNS-based discovery of backend IPs
nextDiscoveryDeadline atomic . Uint64
// vOriginal contains the original yaml value for URLPrefix.
vOriginal interface { }
2023-12-08 21:27:53 +00:00
}
func ( up * URLPrefix ) setLoadBalancingPolicy ( loadBalancingPolicy string ) error {
switch loadBalancingPolicy {
case "" , // empty string is equivalent to least_loaded
"least_loaded" ,
"first_available" :
up . loadBalancingPolicy = loadBalancingPolicy
return nil
default :
return fmt . Errorf ( "unexpected load_balancing_policy: %q; want least_loaded or first_available" , loadBalancingPolicy )
}
2021-04-21 07:55:29 +00:00
}
2023-02-11 08:27:40 +00:00
type backendURL struct {
2024-02-24 00:44:19 +00:00
brokenDeadline atomic . Uint64
concurrentRequests atomic . Int32
url * url . URL
2023-02-11 08:27:40 +00:00
}
func ( bu * backendURL ) isBroken ( ) bool {
ct := fasttime . UnixTimestamp ( )
2024-02-24 00:44:19 +00:00
return ct < bu . brokenDeadline . Load ( )
2023-02-11 08:27:40 +00:00
}
func ( bu * backendURL ) setBroken ( ) {
2023-08-02 12:30:21 +00:00
deadline := fasttime . UnixTimestamp ( ) + uint64 ( ( * failTimeout ) . Seconds ( ) )
2024-02-24 00:44:19 +00:00
bu . brokenDeadline . Store ( deadline )
2023-02-11 08:27:40 +00:00
}
2023-12-08 21:27:53 +00:00
func ( bu * backendURL ) get ( ) {
2024-02-24 00:44:19 +00:00
bu . concurrentRequests . Add ( 1 )
2023-12-08 21:27:53 +00:00
}
2023-02-11 08:27:40 +00:00
func ( bu * backendURL ) put ( ) {
2024-02-24 00:44:19 +00:00
bu . concurrentRequests . Add ( - 1 )
2023-02-11 08:27:40 +00:00
}
func ( up * URLPrefix ) getBackendsCount ( ) int {
2024-03-06 23:02:13 +00:00
pbus := up . bus . Load ( )
return len ( * pbus )
2023-02-11 08:27:40 +00:00
}
2023-12-08 21:27:53 +00:00
// getBackendURL returns the backendURL depending on the load balance policy.
//
// backendURL.put() must be called on the returned backendURL after the request is complete.
func ( up * URLPrefix ) getBackendURL ( ) * backendURL {
2024-03-06 23:02:13 +00:00
up . discoverBackendIPsIfNeeded ( )
pbus := up . bus . Load ( )
bus := * pbus
2023-12-08 21:27:53 +00:00
if up . loadBalancingPolicy == "first_available" {
2024-03-06 23:02:13 +00:00
return getFirstAvailableBackendURL ( bus )
2023-12-08 21:27:53 +00:00
}
2024-03-06 23:02:13 +00:00
return getLeastLoadedBackendURL ( bus , & up . n )
}
func ( up * URLPrefix ) discoverBackendIPsIfNeeded ( ) {
if ! up . discoverBackendIPs {
// The discovery is disabled.
return
}
ct := fasttime . UnixTimestamp ( )
deadline := up . nextDiscoveryDeadline . Load ( )
if ct < deadline {
// There is no need in discovering backends.
return
}
intervalSec := math . Ceil ( discoverBackendIPsInterval . Seconds ( ) )
if intervalSec <= 0 {
intervalSec = 1
}
nextDeadline := ct + uint64 ( intervalSec )
if ! up . nextDiscoveryDeadline . CompareAndSwap ( deadline , nextDeadline ) {
// Concurrent goroutine already started the discovery.
return
}
// Discover ips for all the backendURLs
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Second * time . Duration ( intervalSec ) )
hostToIPs := make ( map [ string ] [ ] string )
for _ , bu := range up . busOriginal {
host := bu . Hostname ( )
if hostToIPs [ host ] != nil {
// ips for the given host have been already discovered
continue
}
addrs , err := resolver . LookupIPAddr ( ctx , host )
var ips [ ] string
if err != nil {
logger . Warnf ( "cannot discover backend IPs for %s: %s; use it literally" , bu , err )
ips = [ ] string { host }
} else {
ips = make ( [ ] string , len ( addrs ) )
for i , addr := range addrs {
ips [ i ] = addr . String ( )
}
// sort ips, so they could be compared below in areEqualBackendURLs()
sort . Strings ( ips )
}
hostToIPs [ host ] = ips
}
cancel ( )
// generate new backendURLs for the resolved IPs
var busNew [ ] * backendURL
for _ , bu := range up . busOriginal {
host := bu . Hostname ( )
port := bu . Port ( )
for _ , ip := range hostToIPs [ host ] {
buCopy := * bu
buCopy . Host = ip
if port != "" {
buCopy . Host += ":" + port
}
busNew = append ( busNew , & backendURL {
url : & buCopy ,
} )
}
}
pbus := up . bus . Load ( )
if areEqualBackendURLs ( * pbus , busNew ) {
return
}
// Store new backend urls
up . bus . Store ( & busNew )
}
func areEqualBackendURLs ( a , b [ ] * backendURL ) bool {
if len ( a ) != len ( b ) {
return false
}
for i , aURL := range a {
bURL := b [ i ]
if aURL . url . String ( ) != bURL . url . String ( ) {
return false
}
}
return true
}
var resolver = & net . Resolver {
PreferGo : true ,
StrictErrors : true ,
2023-12-08 21:27:53 +00:00
}
// getFirstAvailableBackendURL returns the first available backendURL, which isn't broken.
//
// backendURL.put() must be called on the returned backendURL after the request is complete.
2024-03-06 23:02:13 +00:00
func getFirstAvailableBackendURL ( bus [ ] * backendURL ) * backendURL {
2023-12-08 21:27:53 +00:00
bu := bus [ 0 ]
if ! bu . isBroken ( ) {
// Fast path - send the request to the first url.
bu . get ( )
return bu
}
// Slow path - the first url is temporarily unavailabel. Fall back to the remaining urls.
for i := 1 ; i < len ( bus ) ; i ++ {
if ! bus [ i ] . isBroken ( ) {
bu = bus [ i ]
break
}
}
bu . get ( )
return bu
}
2023-02-11 08:27:40 +00:00
// getLeastLoadedBackendURL returns the backendURL with the minimum number of concurrent requests.
//
// backendURL.put() must be called on the returned backendURL after the request is complete.
2024-03-06 23:02:13 +00:00
func getLeastLoadedBackendURL ( bus [ ] * backendURL , atomicCounter * atomic . Uint32 ) * backendURL {
2023-02-11 08:27:40 +00:00
if len ( bus ) == 1 {
// Fast path - return the only backend url.
bu := bus [ 0 ]
2023-12-08 21:27:53 +00:00
bu . get ( )
2023-02-11 08:27:40 +00:00
return bu
}
// Slow path - select other backend urls.
2024-03-06 23:02:13 +00:00
n := atomicCounter . Add ( 1 )
2023-02-11 08:27:40 +00:00
for i := uint32 ( 0 ) ; i < uint32 ( len ( bus ) ) ; i ++ {
idx := ( n + i ) % uint32 ( len ( bus ) )
bu := bus [ idx ]
if bu . isBroken ( ) {
continue
}
2024-02-24 00:44:19 +00:00
if bu . concurrentRequests . Load ( ) == 0 {
2023-02-11 08:27:40 +00:00
// Fast path - return the backend with zero concurrently executed requests.
2024-02-24 00:44:19 +00:00
// Do not use CompareAndSwap() instead of Load(), since it is much slower on systems with many CPU cores.
bu . concurrentRequests . Add ( 1 )
2023-02-11 08:27:40 +00:00
return bu
}
}
// Slow path - return the backend with the minimum number of concurrently executed requests.
buMin := bus [ n % uint32 ( len ( bus ) ) ]
2024-02-24 00:44:19 +00:00
minRequests := buMin . concurrentRequests . Load ( )
2023-02-11 08:27:40 +00:00
for _ , bu := range bus {
if bu . isBroken ( ) {
continue
}
2024-02-24 00:44:19 +00:00
if n := bu . concurrentRequests . Load ( ) ; n < minRequests {
2023-02-11 08:27:40 +00:00
buMin = bu
minRequests = n
}
}
2023-12-08 21:27:53 +00:00
buMin . get ( )
2023-02-11 08:27:40 +00:00
return buMin
2021-05-28 22:00:23 +00:00
}
// UnmarshalYAML unmarshals up from yaml.
func ( up * URLPrefix ) UnmarshalYAML ( f func ( interface { } ) error ) error {
var v interface { }
if err := f ( & v ) ; err != nil {
2021-04-21 07:55:29 +00:00
return err
}
2024-03-06 23:02:13 +00:00
up . vOriginal = v
2023-12-08 21:27:53 +00:00
2021-05-28 22:00:23 +00:00
var urls [ ] string
switch x := v . ( type ) {
case string :
urls = [ ] string { x }
case [ ] interface { } :
if len ( x ) == 0 {
return fmt . Errorf ( "`url_prefix` must contain at least a single url" )
}
us := make ( [ ] string , len ( x ) )
for i , xx := range x {
s , ok := xx . ( string )
if ! ok {
return fmt . Errorf ( "`url_prefix` must contain array of strings; got %T" , xx )
}
us [ i ] = s
}
urls = us
default :
return fmt . Errorf ( "unexpected type for `url_prefix`: %T; want string or []string" , v )
}
2023-12-08 21:27:53 +00:00
2024-03-06 23:02:13 +00:00
bus := make ( [ ] * url . URL , len ( urls ) )
2021-05-28 22:00:23 +00:00
for i , u := range urls {
pu , err := url . Parse ( u )
if err != nil {
return fmt . Errorf ( "cannot unmarshal %q into url: %w" , u , err )
}
2024-03-06 23:02:13 +00:00
bus [ i ] = pu
2021-04-21 07:55:29 +00:00
}
2024-03-06 23:02:13 +00:00
up . busOriginal = bus
2021-04-21 07:55:29 +00:00
return nil
}
2021-05-28 22:00:23 +00:00
// MarshalYAML marshals up to yaml.
func ( up * URLPrefix ) MarshalYAML ( ) ( interface { } , error ) {
2024-03-06 23:02:13 +00:00
return up . vOriginal , nil
2021-04-21 07:55:29 +00:00
}
2023-12-13 22:46:36 +00:00
func ( r * Regex ) match ( s string ) bool {
prefix , ok := r . re . LiteralPrefix ( )
2021-03-05 16:21:11 +00:00
if ok {
// Fast path - literal match
return s == prefix
}
if ! strings . HasPrefix ( s , prefix ) {
return false
}
2023-12-13 22:46:36 +00:00
return r . re . MatchString ( s )
2021-03-05 16:21:11 +00:00
}
// UnmarshalYAML implements yaml.Unmarshaler
2023-12-13 22:46:36 +00:00
func ( r * Regex ) UnmarshalYAML ( f func ( interface { } ) error ) error {
2021-03-05 16:21:11 +00:00
var s string
if err := f ( & s ) ; err != nil {
return err
}
2024-03-06 23:02:13 +00:00
r . sOriginal = s
2021-03-05 16:21:11 +00:00
sAnchored := "^(?:" + s + ")$"
re , err := regexp . Compile ( sAnchored )
if err != nil {
return fmt . Errorf ( "cannot build regexp from %q: %w" , s , err )
}
2023-12-13 22:46:36 +00:00
r . re = re
2021-03-05 16:21:11 +00:00
return nil
}
// MarshalYAML implements yaml.Marshaler.
2023-12-13 22:46:36 +00:00
func ( r * Regex ) MarshalYAML ( ) ( interface { } , error ) {
return r . sOriginal , nil
2021-02-11 10:40:59 +00:00
}
2023-09-20 13:04:52 +00:00
var (
configReloads = metrics . NewCounter ( ` vmauth_config_last_reload_total ` )
configReloadErrors = metrics . NewCounter ( ` vmauth_config_last_reload_errors_total ` )
2023-12-20 12:23:38 +00:00
configSuccess = metrics . NewGauge ( ` vmauth_config_last_reload_successful ` , nil )
2023-09-20 13:04:52 +00:00
configTimestamp = metrics . NewCounter ( ` vmauth_config_last_reload_success_timestamp_seconds ` )
)
2020-05-05 07:53:42 +00:00
func initAuthConfig ( ) {
if len ( * authConfigPath ) == 0 {
2020-06-05 17:13:03 +00:00
logger . Fatalf ( "missing required `-auth.config` command-line flag" )
2020-05-05 07:53:42 +00:00
}
2021-05-21 13:34:03 +00:00
// Register SIGHUP handler for config re-read just before readAuthConfig call.
// This guarantees that the config will be re-read if the signal arrives during readAuthConfig call.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1240
sighupCh := procutil . NewSighupChan ( )
2023-09-20 13:04:52 +00:00
_ , err := loadAuthConfig ( )
2020-05-05 07:53:42 +00:00
if err != nil {
2023-05-08 17:04:25 +00:00
logger . Fatalf ( "cannot load auth config: %s" , err )
2020-05-05 07:53:42 +00:00
}
2023-04-20 17:08:27 +00:00
2023-09-20 13:04:52 +00:00
configSuccess . Set ( 1 )
configTimestamp . Set ( fasttime . UnixTimestamp ( ) )
2020-05-05 07:53:42 +00:00
stopCh = make ( chan struct { } )
authConfigWG . Add ( 1 )
go func ( ) {
defer authConfigWG . Done ( )
2021-05-21 13:34:03 +00:00
authConfigReloader ( sighupCh )
2020-05-05 07:53:42 +00:00
} ( )
}
func stopAuthConfig ( ) {
close ( stopCh )
authConfigWG . Wait ( )
}
2021-05-21 13:34:03 +00:00
func authConfigReloader ( sighupCh <- chan os . Signal ) {
2023-03-23 08:34:12 +00:00
var refreshCh <- chan time . Time
// initialize auth refresh interval
if * configCheckInterval > 0 {
ticker := time . NewTicker ( * configCheckInterval )
defer ticker . Stop ( )
refreshCh = ticker . C
}
2023-09-20 13:04:52 +00:00
updateFn := func ( ) {
configReloads . Inc ( )
updated , err := loadAuthConfig ( )
if err != nil {
logger . Errorf ( "failed to load auth config; using the last successfully loaded config; error: %s" , err )
configSuccess . Set ( 0 )
configReloadErrors . Inc ( )
return
}
configSuccess . Set ( 1 )
if updated {
configTimestamp . Set ( fasttime . UnixTimestamp ( ) )
}
}
2020-05-05 07:53:42 +00:00
for {
select {
case <- stopCh :
return
2023-03-23 08:34:12 +00:00
case <- refreshCh :
2023-09-20 13:04:52 +00:00
updateFn ( )
2020-05-05 07:53:42 +00:00
case <- sighupCh :
2020-06-03 20:22:09 +00:00
logger . Infof ( "SIGHUP received; loading -auth.config=%q" , * authConfigPath )
2023-09-20 13:04:52 +00:00
updateFn ( )
2020-05-05 07:53:42 +00:00
}
}
}
2023-09-20 13:04:52 +00:00
// authConfigData stores the yaml definition for this config.
// authConfigData needs to be updated each time authConfig is updated.
var authConfigData atomic . Pointer [ [ ] byte ]
2024-01-21 02:40:52 +00:00
var (
authConfig atomic . Pointer [ AuthConfig ]
authUsers atomic . Pointer [ map [ string ] * UserInfo ]
authConfigWG sync . WaitGroup
stopCh chan struct { }
)
2020-05-05 07:53:42 +00:00
2023-09-20 13:04:52 +00:00
// loadAuthConfig loads and applies the config from *authConfigPath.
// It returns bool value to identify if new config was applied.
// The config can be not applied if there is a parsing error
// or if there are no changes to the current authConfig.
func loadAuthConfig ( ) ( bool , error ) {
2024-01-21 19:58:26 +00:00
data , err := fscore . ReadFileOrHTTP ( * authConfigPath )
2023-09-20 13:04:52 +00:00
if err != nil {
2023-09-21 09:04:13 +00:00
return false , fmt . Errorf ( "failed to read -auth.config=%q: %w" , * authConfigPath , err )
2023-09-20 13:04:52 +00:00
}
oldData := authConfigData . Load ( )
if oldData != nil && bytes . Equal ( data , * oldData ) {
// there are no updates in the config - skip reloading.
return false , nil
}
ac , err := parseAuthConfig ( data )
2020-05-05 07:53:42 +00:00
if err != nil {
2023-09-21 09:04:13 +00:00
return false , fmt . Errorf ( "failed to parse -auth.config=%q: %w" , * authConfigPath , err )
2020-05-05 07:53:42 +00:00
}
2023-04-20 17:08:27 +00:00
m , err := parseAuthConfigUsers ( ac )
2020-05-05 07:53:42 +00:00
if err != nil {
2023-09-21 09:04:13 +00:00
return false , fmt . Errorf ( "failed to parse users from -auth.config=%q: %w" , * authConfigPath , err )
2020-05-05 07:53:42 +00:00
}
2023-05-08 17:04:25 +00:00
logger . Infof ( "loaded information about %d users from -auth.config=%q" , len ( m ) , * authConfigPath )
2023-04-20 17:08:27 +00:00
2024-02-13 18:49:17 +00:00
prevAc := authConfig . Load ( )
if prevAc != nil {
metrics . UnregisterSet ( prevAc . ms )
}
metrics . RegisterSet ( ac . ms )
2023-04-20 17:08:27 +00:00
authConfig . Store ( ac )
2023-09-20 13:04:52 +00:00
authConfigData . Store ( & data )
2023-04-20 17:08:27 +00:00
authUsers . Store ( & m )
2023-09-20 13:04:52 +00:00
return true , nil
2023-04-20 17:08:27 +00:00
}
func parseAuthConfig ( data [ ] byte ) ( * AuthConfig , error ) {
data , err := envtemplate . ReplaceBytes ( data )
2022-10-18 07:28:39 +00:00
if err != nil {
return nil , fmt . Errorf ( "cannot expand environment vars: %w" , err )
}
2024-02-13 18:49:17 +00:00
ac := & AuthConfig {
ms : metrics . NewSet ( ) ,
}
if err = yaml . UnmarshalStrict ( data , ac ) ; err != nil {
2020-06-30 19:58:18 +00:00
return nil , fmt . Errorf ( "cannot unmarshal AuthConfig data: %w" , err )
2020-05-05 07:53:42 +00:00
}
2024-02-13 18:49:17 +00:00
2023-04-24 12:57:13 +00:00
ui := ac . UnauthorizedUser
if ui != nil {
2023-11-01 19:59:46 +00:00
if ui . Username != "" {
return nil , fmt . Errorf ( "field username can't be specified for unauthorized_user section" )
}
if ui . Password != "" {
return nil , fmt . Errorf ( "field password can't be specified for unauthorized_user section" )
}
if ui . BearerToken != "" {
return nil , fmt . Errorf ( "field bearer_token can't be specified for unauthorized_user section" )
}
2024-04-02 18:15:44 +00:00
if ui . AuthToken != "" {
return nil , fmt . Errorf ( "field auth_token can't be specified for unauthorized_user section" )
}
2023-11-01 19:59:46 +00:00
if ui . Name != "" {
return nil , fmt . Errorf ( "field name can't be specified for unauthorized_user section" )
}
2023-12-08 21:27:53 +00:00
if err := ui . initURLs ( ) ; err != nil {
return nil , err
}
2024-01-21 02:40:52 +00:00
metricLabels , err := ui . getMetricLabels ( )
if err != nil {
return nil , fmt . Errorf ( "cannot parse metric_labels for unauthorized_user: %w" , err )
}
2024-02-13 18:49:17 +00:00
ui . requests = ac . ms . NewCounter ( ` vmauth_unauthorized_user_requests_total ` + metricLabels )
ui . backendErrors = ac . ms . NewCounter ( ` vmauth_unauthorized_user_request_backend_errors_total ` + metricLabels )
ui . requestsDuration = ac . ms . NewSummary ( ` vmauth_unauthorized_user_request_duration_seconds ` + metricLabels )
2023-04-24 12:57:13 +00:00
ui . concurrencyLimitCh = make ( chan struct { } , ui . getMaxConcurrentRequests ( ) )
2024-02-13 18:49:17 +00:00
ui . concurrencyLimitReached = ac . ms . NewCounter ( ` vmauth_unauthorized_user_concurrent_requests_limit_reached_total ` + metricLabels )
_ = ac . ms . NewGauge ( ` vmauth_unauthorized_user_concurrent_requests_capacity ` + metricLabels , func ( ) float64 {
2023-04-24 12:57:13 +00:00
return float64 ( cap ( ui . concurrencyLimitCh ) )
} )
2024-02-13 18:49:17 +00:00
_ = ac . ms . NewGauge ( ` vmauth_unauthorized_user_concurrent_requests_current ` + metricLabels , func ( ) float64 {
2023-04-24 12:57:13 +00:00
return float64 ( len ( ui . concurrencyLimitCh ) )
} )
2024-01-21 02:40:52 +00:00
2023-11-13 07:23:35 +00:00
tr , err := getTransport ( ui . TLSInsecureSkipVerify , ui . TLSCAFile )
if err != nil {
return nil , fmt . Errorf ( "cannot initialize HTTP transport: %w" , err )
}
ui . httpTransport = tr
2023-04-24 12:57:13 +00:00
}
2024-02-13 18:49:17 +00:00
return ac , nil
2023-04-20 17:08:27 +00:00
}
func parseAuthConfigUsers ( ac * AuthConfig ) ( map [ string ] * UserInfo , error ) {
2020-05-05 07:53:42 +00:00
uis := ac . Users
2023-05-18 16:44:01 +00:00
if len ( uis ) == 0 && ac . UnauthorizedUser == nil {
return nil , fmt . Errorf ( "Missing `users` or `unauthorized_user` sections" )
2020-05-05 07:53:42 +00:00
}
2021-04-02 19:14:53 +00:00
byAuthToken := make ( map [ string ] * UserInfo , len ( uis ) )
2020-05-05 07:53:42 +00:00
for i := range uis {
ui := & uis [ i ]
2024-04-02 18:15:44 +00:00
ats , err := getAuthTokens ( ui . AuthToken , ui . BearerToken , ui . Username , ui . Password )
2024-03-06 18:05:55 +00:00
if err != nil {
return nil , err
2021-11-17 11:31:16 +00:00
}
2024-02-12 22:57:53 +00:00
for _ , at := range ats {
if uiOld := byAuthToken [ at ] ; uiOld != nil {
return nil , fmt . Errorf ( "duplicate auth token=%q found for username=%q, name=%q; the previous one is set for username=%q, name=%q" ,
at , ui . Username , ui . Name , uiOld . Username , uiOld . Name )
}
2021-04-02 19:14:53 +00:00
}
2023-12-08 21:27:53 +00:00
if err := ui . initURLs ( ) ; err != nil {
return nil , err
2020-05-05 07:53:42 +00:00
}
2023-12-08 21:27:53 +00:00
2024-01-21 02:40:52 +00:00
metricLabels , err := ui . getMetricLabels ( )
if err != nil {
return nil , fmt . Errorf ( "cannot parse metric_labels: %w" , err )
2021-04-02 19:14:53 +00:00
}
2024-02-13 18:49:17 +00:00
ui . requests = ac . ms . GetOrCreateCounter ( ` vmauth_user_requests_total ` + metricLabels )
ui . backendErrors = ac . ms . GetOrCreateCounter ( ` vmauth_user_request_backend_errors_total ` + metricLabels )
ui . requestsDuration = ac . ms . GetOrCreateSummary ( ` vmauth_user_request_duration_seconds ` + metricLabels )
2023-02-11 05:57:49 +00:00
mcr := ui . getMaxConcurrentRequests ( )
ui . concurrencyLimitCh = make ( chan struct { } , mcr )
2024-02-13 18:49:17 +00:00
ui . concurrencyLimitReached = ac . ms . GetOrCreateCounter ( ` vmauth_user_concurrent_requests_limit_reached_total ` + metricLabels )
_ = ac . ms . GetOrCreateGauge ( ` vmauth_user_concurrent_requests_capacity ` + metricLabels , func ( ) float64 {
2023-02-11 05:57:49 +00:00
return float64 ( cap ( ui . concurrencyLimitCh ) )
} )
2024-02-13 18:49:17 +00:00
_ = ac . ms . GetOrCreateGauge ( ` vmauth_user_concurrent_requests_current ` + metricLabels , func ( ) float64 {
2023-02-11 05:57:49 +00:00
return float64 ( len ( ui . concurrencyLimitCh ) )
} )
2023-12-08 21:27:53 +00:00
2023-11-13 07:23:35 +00:00
tr , err := getTransport ( ui . TLSInsecureSkipVerify , ui . TLSCAFile )
if err != nil {
return nil , fmt . Errorf ( "cannot initialize HTTP transport: %w" , err )
}
ui . httpTransport = tr
2023-11-03 11:04:17 +00:00
2024-02-12 22:57:53 +00:00
for _ , at := range ats {
byAuthToken [ at ] = ui
}
2020-05-05 07:53:42 +00:00
}
2021-04-02 19:14:53 +00:00
return byAuthToken , nil
}
2024-01-21 02:40:52 +00:00
var labelNameRegexp = regexp . MustCompile ( "^[a-zA-Z_:.][a-zA-Z0-9_:.]*$" )
func ( ui * UserInfo ) getMetricLabels ( ) ( string , error ) {
name := ui . name ( )
if len ( name ) == 0 && len ( ui . MetricLabels ) == 0 {
// fast path
return "" , nil
}
labels := make ( [ ] string , 0 , len ( ui . MetricLabels ) + 1 )
if len ( name ) > 0 {
labels = append ( labels , fmt . Sprintf ( ` username=%q ` , name ) )
}
for k , v := range ui . MetricLabels {
if ! labelNameRegexp . MatchString ( k ) {
return "" , fmt . Errorf ( "incorrect label name=%q, it must match regex=%q for user=%q" , k , labelNameRegexp , name )
}
labels = append ( labels , fmt . Sprintf ( ` %s=%q ` , k , v ) )
}
sort . Strings ( labels )
labelsStr := "{" + strings . Join ( labels , "," ) + "}"
return labelsStr , nil
}
2023-12-08 21:27:53 +00:00
func ( ui * UserInfo ) initURLs ( ) error {
retryStatusCodes := defaultRetryStatusCodes . Values ( )
loadBalancingPolicy := * defaultLoadBalancingPolicy
2023-12-13 23:04:46 +00:00
dropSrcPathPrefixParts := 0
2024-03-06 23:02:13 +00:00
discoverBackendIPs := * discoverBackendIPsGlobal
2023-12-08 21:27:53 +00:00
if ui . URLPrefix != nil {
2024-03-06 23:02:13 +00:00
if err := ui . URLPrefix . sanitizeAndInitialize ( ) ; err != nil {
2023-12-08 21:27:53 +00:00
return err
}
2023-12-13 23:04:46 +00:00
if ui . RetryStatusCodes != nil {
2023-12-08 21:27:53 +00:00
retryStatusCodes = ui . RetryStatusCodes
}
if ui . LoadBalancingPolicy != "" {
loadBalancingPolicy = ui . LoadBalancingPolicy
}
2023-12-13 23:04:46 +00:00
if ui . DropSrcPathPrefixParts != nil {
dropSrcPathPrefixParts = * ui . DropSrcPathPrefixParts
}
2024-03-06 23:02:13 +00:00
if ui . DiscoverBackendIPs != nil {
discoverBackendIPs = * ui . DiscoverBackendIPs
}
2023-12-08 21:27:53 +00:00
ui . URLPrefix . retryStatusCodes = retryStatusCodes
2023-12-13 23:04:46 +00:00
ui . URLPrefix . dropSrcPathPrefixParts = dropSrcPathPrefixParts
2024-03-06 23:02:13 +00:00
ui . URLPrefix . discoverBackendIPs = discoverBackendIPs
2023-12-08 21:27:53 +00:00
if err := ui . URLPrefix . setLoadBalancingPolicy ( loadBalancingPolicy ) ; err != nil {
return err
}
}
if ui . DefaultURL != nil {
2024-03-06 23:02:13 +00:00
if err := ui . DefaultURL . sanitizeAndInitialize ( ) ; err != nil {
2023-12-08 21:27:53 +00:00
return err
}
}
for _ , e := range ui . URLMaps {
2024-03-06 19:56:32 +00:00
if len ( e . SrcPaths ) == 0 && len ( e . SrcHosts ) == 0 && len ( e . SrcQueryArgs ) == 0 && len ( e . SrcHeaders ) == 0 {
return fmt . Errorf ( "missing `src_paths`, `src_hosts`, `src_query_args` and `src_headers` in `url_map`" )
2023-12-08 21:27:53 +00:00
}
if e . URLPrefix == nil {
return fmt . Errorf ( "missing `url_prefix` in `url_map`" )
}
2024-03-06 23:02:13 +00:00
if err := e . URLPrefix . sanitizeAndInitialize ( ) ; err != nil {
2023-12-08 21:27:53 +00:00
return err
}
rscs := retryStatusCodes
lbp := loadBalancingPolicy
2023-12-13 23:04:46 +00:00
dsp := dropSrcPathPrefixParts
2024-03-06 23:02:13 +00:00
dbd := discoverBackendIPs
2023-12-13 23:04:46 +00:00
if e . RetryStatusCodes != nil {
2023-12-08 21:27:53 +00:00
rscs = e . RetryStatusCodes
}
if e . LoadBalancingPolicy != "" {
lbp = e . LoadBalancingPolicy
}
2023-12-13 23:04:46 +00:00
if e . DropSrcPathPrefixParts != nil {
dsp = * e . DropSrcPathPrefixParts
}
2024-03-06 23:02:13 +00:00
if e . DiscoverBackendIPs != nil {
dbd = * e . DiscoverBackendIPs
}
2023-12-08 21:27:53 +00:00
e . URLPrefix . retryStatusCodes = rscs
if err := e . URLPrefix . setLoadBalancingPolicy ( lbp ) ; err != nil {
return err
}
2023-12-13 23:04:46 +00:00
e . URLPrefix . dropSrcPathPrefixParts = dsp
2024-03-06 23:02:13 +00:00
e . URLPrefix . discoverBackendIPs = dbd
2023-12-08 21:27:53 +00:00
}
if len ( ui . URLMaps ) == 0 && ui . URLPrefix == nil {
2024-03-06 18:05:55 +00:00
return fmt . Errorf ( "missing `url_prefix` or `url_map`" )
2023-12-08 21:27:53 +00:00
}
return nil
}
2023-02-10 04:03:01 +00:00
func ( ui * UserInfo ) name ( ) string {
if ui . Name != "" {
return ui . Name
}
if ui . Username != "" {
return ui . Username
}
if ui . BearerToken != "" {
2024-01-21 02:40:52 +00:00
h := xxhash . Sum64 ( [ ] byte ( ui . BearerToken ) )
return fmt . Sprintf ( "bearer_token:hash:%016X" , h )
2023-02-10 04:03:01 +00:00
}
2024-04-02 18:15:44 +00:00
if ui . AuthToken != "" {
h := xxhash . Sum64 ( [ ] byte ( ui . AuthToken ) )
return fmt . Sprintf ( "auth_token:hash:%016X" , h )
}
2023-02-10 04:03:01 +00:00
return ""
}
2024-04-02 18:15:44 +00:00
func getAuthTokens ( authToken , bearerToken , username , password string ) ( [ ] string , error ) {
if authToken != "" {
if bearerToken != "" {
return nil , fmt . Errorf ( "bearer_token cannot be specified if auth_token is set" )
}
if username != "" || password != "" {
return nil , fmt . Errorf ( "username and password cannot be specified if auth_token is set" )
}
at := getHTTPAuthToken ( authToken )
return [ ] string { at } , nil
}
2021-11-17 11:31:16 +00:00
if bearerToken != "" {
2024-03-06 18:05:55 +00:00
if username != "" || password != "" {
return nil , fmt . Errorf ( "username and password cannot be specified if bearer_token is set" )
}
2021-11-17 11:31:16 +00:00
// Accept the bearerToken as Basic Auth username with empty password
2024-02-12 22:57:53 +00:00
at1 := getHTTPAuthBearerToken ( bearerToken )
at2 := getHTTPAuthBasicToken ( bearerToken , "" )
2024-03-06 18:05:55 +00:00
return [ ] string { at1 , at2 } , nil
}
if username != "" {
2024-02-12 22:57:53 +00:00
at := getHTTPAuthBasicToken ( username , password )
2024-03-06 18:05:55 +00:00
return [ ] string { at } , nil
2021-11-17 11:31:16 +00:00
}
2024-03-06 18:05:55 +00:00
return nil , fmt . Errorf ( "missing authorization options; bearer_token or username must be set" )
2021-11-17 11:31:16 +00:00
}
2024-04-02 18:15:44 +00:00
func getHTTPAuthToken ( authToken string ) string {
return "http_auth:" + authToken
}
2024-02-12 22:57:53 +00:00
func getHTTPAuthBearerToken ( bearerToken string ) string {
return "http_auth:Bearer " + bearerToken
}
func getHTTPAuthBasicToken ( username , password string ) string {
2021-04-02 19:14:53 +00:00
token := username + ":" + password
token64 := base64 . StdEncoding . EncodeToString ( [ ] byte ( token ) )
2024-02-12 22:57:53 +00:00
return "http_auth:Basic " + token64
}
2024-04-02 16:26:49 +00:00
var defaultHeaderNames = [ ] string { "Authorization" }
2024-02-12 22:57:53 +00:00
func getAuthTokensFromRequest ( r * http . Request ) [ ] string {
var ats [ ] string
2024-04-02 16:26:49 +00:00
// Obtain possible auth tokens from one of the allowed auth headers
headerNames := * httpAuthHeader
if len ( headerNames ) == 0 {
headerNames = defaultHeaderNames
}
for _ , headerName := range headerNames {
if ah := r . Header . Get ( headerName ) ; ah != "" {
if strings . HasPrefix ( ah , "Token " ) {
// Handle InfluxDB's proprietary token authentication scheme as a bearer token authentication
// See https://docs.influxdata.com/influxdb/v2.0/api/
ah = strings . Replace ( ah , "Token" , "Bearer" , 1 )
}
at := "http_auth:" + ah
ats = append ( ats , at )
2024-04-02 15:30:42 +00:00
}
2024-02-12 22:57:53 +00:00
}
2024-03-06 18:05:55 +00:00
2024-02-12 22:57:53 +00:00
return ats
2020-05-05 07:53:42 +00:00
}
2021-02-11 10:40:59 +00:00
2024-03-06 23:02:13 +00:00
func ( up * URLPrefix ) sanitizeAndInitialize ( ) error {
for i , bu := range up . busOriginal {
puNew , err := sanitizeURLPrefix ( bu )
2021-05-28 22:00:23 +00:00
if err != nil {
return err
}
2024-03-06 23:02:13 +00:00
up . busOriginal [ i ] = puNew
2021-05-28 22:00:23 +00:00
}
2024-03-06 23:02:13 +00:00
// Initialize up.bus
bus := make ( [ ] * backendURL , len ( up . busOriginal ) )
for i , bu := range up . busOriginal {
bus [ i ] = & backendURL {
url : bu ,
}
}
up . bus . Store ( & bus )
2021-05-28 22:00:23 +00:00
return nil
}
2021-04-21 07:55:29 +00:00
func sanitizeURLPrefix ( urlPrefix * url . URL ) ( * url . URL , error ) {
2021-02-11 10:40:59 +00:00
// Remove trailing '/' from urlPrefix
2021-04-21 07:55:29 +00:00
for strings . HasSuffix ( urlPrefix . Path , "/" ) {
urlPrefix . Path = urlPrefix . Path [ : len ( urlPrefix . Path ) - 1 ]
2021-02-11 10:40:59 +00:00
}
// Validate urlPrefix
2021-04-21 07:55:29 +00:00
if urlPrefix . Scheme != "http" && urlPrefix . Scheme != "https" {
return nil , fmt . Errorf ( "unsupported scheme for `url_prefix: %q`: %q; must be `http` or `https`" , urlPrefix , urlPrefix . Scheme )
2021-02-11 10:40:59 +00:00
}
2021-04-21 07:55:29 +00:00
if urlPrefix . Host == "" {
return nil , fmt . Errorf ( "missing hostname in `url_prefix %q`" , urlPrefix . Host )
2021-02-11 10:40:59 +00:00
}
return urlPrefix , nil
}