func (cfg Config) ValidWebhookURL(testURL string) bool {
	if len(cfg.Webhook.AllowedURLs) == 0 {
		return true
	}
	for _, baseU := range cfg.Webhook.AllowedURLs {
		matched, err := MatchURL(baseU, testURL)
		if err != nil {
			return false
		}
		if matched {
			return true
		}
	}
	return false
}
ValidWebhookURL returns true if the URL is an allowed webhook source.
package config
import (
	"fmt"
	"net/url"
	"strings"
	"github.com/pkg/errors"
	"github.com/target/goalert/validation"
	"github.com/target/goalert/validation/validate"
)
const SchemaVersion = 1
type Config struct {
	data        []byte
	fallbackURL string
	General struct {
		ApplicationName              string `public:"true" info:"The name used in messaging and page titles. Defaults to \"GoAlert\"."`
		PublicURL                    string `public:"true" info:"Publicly routable URL for UI links and API calls."`
		GoogleAnalyticsID            string `public:"true"`
		NotificationDisclaimer       string `public:"true" info:"Disclaimer text for receiving pre-recorded notifications (appears on profile page)."`
		DisableMessageBundles        bool   `public:"true" info:"Disable bundling status updates and alert notifications."`
		ShortURL                     string `public:"true" info:"If set, messages will contain a shorter URL using this as a prefix (e.g. http://example.com). It should point to GoAlert and can be the same as the PublicURL."`
		DisableSMSLinks              bool   `public:"true" info:"If set, SMS messages will not contain a URL pointing to GoAlert."`
		DisableLabelCreation         bool   `public:"true" info:"Disables the ability to create new labels for services."`
		DisableCalendarSubscriptions bool   `public:"true" info:"If set, disables all active calendar subscriptions as well as the ability to create new calendar subscriptions."`
		EnableV1GraphQL              bool   `info:"Enables the deprecated /v1/graphql endpoint (replaced by /api/graphql)."`
	}
	Maintenance struct {
		AlertCleanupDays int `public:"true" info:"Closed alerts will be deleted after this many days (0 means disable cleanup)."`
		APIKeyExpireDays int `public:"true" info:"Unused calendar API keys will be disabled after this many days (0 means disable cleanup)."`
	}
	Auth struct {
		RefererURLs  []string `info:"Allowed referer URLs for auth and redirects."`
		DisableBasic bool     `public:"true" info:"Disallow username/password login."`
	}
	GitHub struct {
		Enable bool `public:"true" info:"Enable GitHub authentication."`
		NewUsers bool `info:"Allow new user creation via GitHub authentication."`
		ClientID     string
		ClientSecret string `password:"true"`
		AllowedUsers []string `info:"Allow any of the listed GitHub usernames to authenticate. Use '*' to allow any user."`
		AllowedOrgs  []string `info:"Allow any member of any listed GitHub org (or team, using the format 'org/team') to authenticate."`
		EnterpriseURL string `info:"GitHub URL (without /api) when used with GitHub Enterprise."`
	}
	OIDC struct {
		Enable bool `public:"true" info:"Enable OpenID Connect authentication."`
		NewUsers     bool   `info:"Allow new user creation via OIDC authentication."`
		OverrideName string `info:"Set the name/label on the login page to something other than OIDC."`
		IssuerURL    string
		ClientID     string
		ClientSecret string `password:"true"`
		Scopes                    string `info:"Requested scopes for authentication. If left blank, openid, profile, and email will be used."`
		UserInfoEmailPath         string `info:"JMESPath expression to find email address in UserInfo. If set, the email claim will be ignored in favor of this. (suggestion: email)."`
		UserInfoEmailVerifiedPath string `info:"JMESPath expression to find email verification state in UserInfo. If set, the email_verified claim will be ignored in favor of this. (suggestion: email_verified)."`
		UserInfoNamePath          string `info:"JMESPath expression to find full name in UserInfo. If set, the name claim will be ignored in favor of this. (suggestion: name || cn || join(' ', [firstname, lastname]))"`
	}
	Mailgun struct {
		Enable bool `public:"true"`
		APIKey      string `password:"true"`
		EmailDomain string `info:"The TO address for all incoming alerts."`
	}
	Slack struct {
		Enable bool `public:"true"`
		ClientID     string
		ClientSecret string `password:"true"`
		AccessToken string `password:"true" info:"Slack app bot user OAuth access token (should start with xoxb-)."`
	}
	Twilio struct {
		Enable bool `public:"true" info:"Enables sending and processing of Voice and SMS messages through the Twilio notification provider."`
		AccountSID string
		AuthToken  string `password:"true" info:"The primary Auth Token for Twilio. Must be primary (not secondary) for request valiation."`
		FromNumber string `public:"true" info:"The Twilio number to use for outgoing notifications."`
		MessagingServiceSID string `public:"true" info:"If set, replaces the use of From Number for SMS notifications."`
		DisableTwoWaySMS      bool     `info:"Disables SMS reply codes for alert messages."`
		SMSCarrierLookup      bool     `info:"Perform carrier lookup of SMS contact methods (required for SMSFromNumberOverride). Extra charges may apply."`
		SMSFromNumberOverride []string `info:"List of 'carrier=number' pairs, SMS messages to numbers of the provided carrier string (exact match) will use the alternate From Number."`
	}
	SMTP struct {
		Enable bool `public:"true" info:"Enables email as a contact method."`
		From string `public:"true" info:"The email address messages should be sent from."`
		Address    string `info:"The server address to use for sending email. Port is optional."`
		DisableTLS bool   `info:"Disables TLS on the connection (STARTTLS will still be used if supported)."`
		SkipVerify bool   `info:"Disables certificate validation for TLS/STARTTLS (insecure)."`
		Username string `info:"Username for authentication."`
		Password string `password:"true" info:"Password for authentication."`
	}
	Webhook struct {
		Enable      bool     `public:"true" info:"Enables webhook as a contact method."`
		AllowedURLs []string `public:"true" info:"If set, allows webhooks for these domains only."`
	}
	Feedback struct {
		Enable      bool   `public:"true" info:"Enables Feedback link in nav bar."`
		OverrideURL string `public:"true" info:"Use a custom URL for Feedback link in nav bar."`
	}
}
func (cfg Config) TwilioSMSFromNumber(carrier string) string {
	if carrier != "" {
		for _, s := range cfg.Twilio.SMSFromNumberOverride {
			parts := strings.SplitN(s, "=", 2)
			if len(parts) != 2 {
				continue
			}
			if parts[0] != carrier {
				continue
			}
			return parts[1]
		}
	}
	if cfg.Twilio.MessagingServiceSID != "" {
		return cfg.Twilio.MessagingServiceSID
	}
	return cfg.Twilio.FromNumber
}
func (cfg Config) rawCallbackURL(path string, mergeParams ...url.Values) *url.URL {
	base, err := url.Parse(cfg.PublicURL())
	if err != nil {
		panic(errors.Wrap(err, "parse PublicURL"))
	}
	next, err := url.Parse(path)
	if err != nil {
		panic(errors.Wrap(err, "parse path"))
	}
	base.Path = strings.TrimSuffix(base.Path, "/") + "/" + strings.TrimPrefix(next.Path, "/")
	params := base.Query()
	nx := next.Query()
	for name, val := range nx {
		params[name] = val
	}
	for _, merge := range mergeParams {
		for name, val := range merge {
			params[name] = val
		}
	}
	base.RawQuery = params.Encode()
	return base
}
func (cfg Config) CallbackURL(path string, mergeParams ...url.Values) string {
	base := cfg.rawCallbackURL(path, mergeParams...)
	newPath := ShortPath(base.Path)
	if newPath != "" && cfg.General.ShortURL != "" {
		short, err := url.Parse(cfg.General.ShortURL)
		if err != nil {
			panic(errors.Wrap(err, "parse ShortURL"))
		}
		base.Path = newPath
		base.Host = short.Host
		base.Scheme = short.Scheme
	}
	return base.String()
}
func MatchURL(baseURL, testURL string) (bool, error) {
	compareQueryValues := func(baseVal, testVal url.Values) bool {
		for name := range baseVal {
			if baseVal.Get(name) == testVal.Get(name) {
				continue
			}
			return false
		}
		return true
	}
	addImplicitPort := func(u *url.URL) {
		if strings.Contains(u.Host, ":") {
			return
		}
		switch strings.ToLower(u.Scheme) {
		case "http":
			u.Host += ":80"
		case "https":
			u.Host += ":443"
		}
	}
	base, err := url.Parse(baseURL)
	if err != nil {
		return false, err
	}
	test, err := url.Parse(testURL)
	if err != nil {
		return false, err
	}
	addImplicitPort(base)
	addImplicitPort(test)
	if !strings.EqualFold(base.Host, test.Host) {
		return false, nil
	}
	if !strings.EqualFold(base.Scheme, test.Scheme) {
		return false, nil
	}
	if len(base.Path) > 1 && !strings.HasPrefix(test.Path, base.Path) {
		return false, nil
	}
	if !compareQueryValues(base.Query(), test.Query()) {
		return false, nil
	}
	return true, nil
}