package config

import (
	"bytes"
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"time"

	envoy_http_connection_manager "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	"github.com/mitchellh/mapstructure"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/volatiletech/null/v9"
	"google.golang.org/protobuf/types/known/durationpb"

	"github.com/pomerium/csrf"
	"github.com/pomerium/pomerium/internal/atomicutil"
	"github.com/pomerium/pomerium/internal/hashutil"
	"github.com/pomerium/pomerium/internal/httputil"
	"github.com/pomerium/pomerium/internal/identity/oauth"
	"github.com/pomerium/pomerium/internal/identity/oauth/apple"
	"github.com/pomerium/pomerium/internal/log"
	"github.com/pomerium/pomerium/internal/sets"
	"github.com/pomerium/pomerium/internal/telemetry"
	"github.com/pomerium/pomerium/internal/telemetry/metrics"
	"github.com/pomerium/pomerium/internal/urlutil"
	"github.com/pomerium/pomerium/pkg/cryptutil"
	"github.com/pomerium/pomerium/pkg/grpc/config"
	"github.com/pomerium/pomerium/pkg/grpc/crypt"
	"github.com/pomerium/pomerium/pkg/hpke"
)

// DisableHeaderKey is the key used to check whether to disable setting header
const DisableHeaderKey = "disable"

// DefaultAlternativeAddr is the address used is two services are competing over
// the same listener. Typically this is invisible to the end user (e.g. localhost)
// gRPC server, or is used for healthchecks (authorize only service)
const DefaultAlternativeAddr = ":5443"

// The randomSharedKey is used if no shared key is supplied in all-in-one mode.
var randomSharedKey = cryptutil.NewBase64Key()

// Options are the global environmental flags used to set up pomerium's services.
// Use NewXXXOptions() methods for a safely initialized data structure.
type Options struct {
	// InstallationID is used to indicate a unique installation of pomerium. Useful for telemetry.
	InstallationID string `mapstructure:"installation_id" yaml:"installation_id,omitempty"`

	// Debug is deprecated.
	Debug bool `mapstructure:"pomerium_debug" yaml:"pomerium_debug,omitempty"`

	// LogLevel sets the global override for log level. All Loggers will use at least this value.
	// Possible options are "info","warn","debug" and "error". Defaults to "info".
	LogLevel LogLevel `mapstructure:"log_level" yaml:"log_level,omitempty"`

	// ProxyLogLevel sets the log level for the proxy service.
	// Possible options are "info","warn", and "error". Defaults to the value of `LogLevel`.
	ProxyLogLevel LogLevel `mapstructure:"proxy_log_level" yaml:"proxy_log_level,omitempty"`

	// AccessLogFields are the fields to log in access logs.
	AccessLogFields []log.AccessLogField `mapstructure:"access_log_fields" yaml:"access_log_fields,omitempty"`

	// AuthorizeLogFields are the fields to log in authorize logs.
	AuthorizeLogFields []log.AuthorizeLogField `mapstructure:"authorize_log_fields" yaml:"authorize_log_fields,omitempty"`

	// SharedKey is the shared secret authorization key used to mutually authenticate
	// requests between services.
	SharedKey        string `mapstructure:"shared_secret" yaml:"shared_secret,omitempty"`
	SharedSecretFile string `mapstructure:"shared_secret_file" yaml:"shared_secret_file,omitempty"`

	// Services is a list enabled service mode. If none are selected, "all" is used.
	// Available options are : "all", "authenticate", "proxy".
	Services string `mapstructure:"services" yaml:"services,omitempty"`

	// Addr specifies the host and port on which the server should serve
	// HTTPS requests. If empty, ":443" (localhost:443) is used.
	Addr string `mapstructure:"address" yaml:"address,omitempty"`

	// InsecureServer when enabled disables all transport security.
	// In this mode, Pomerium is susceptible to man-in-the-middle attacks.
	// This should be used only for testing.
	InsecureServer bool `mapstructure:"insecure_server" yaml:"insecure_server,omitempty"`

	// DNSLookupFamily is the DNS IP address resolution policy.
	// If this setting is not specified, the value defaults to V4_PREFERRED.
	DNSLookupFamily string `mapstructure:"dns_lookup_family" yaml:"dns_lookup_family,omitempty"`

	CertificateData  []*config.Settings_Certificate
	CertificateFiles []certificateFilePair `mapstructure:"certificates" yaml:"certificates,omitempty"`

	// Cert and Key is the x509 certificate used to create the HTTPS server.
	Cert string `mapstructure:"certificate" yaml:"certificate,omitempty"`
	Key  string `mapstructure:"certificate_key" yaml:"certificate_key,omitempty"`

	// CertFile and KeyFile is the x509 certificate used to hydrate TLSCertificate
	CertFile string `mapstructure:"certificate_file" yaml:"certificate_file,omitempty"`
	KeyFile  string `mapstructure:"certificate_key_file" yaml:"certificate_key_file,omitempty"`

	// HttpRedirectAddr, if set, specifies the host and port to run the HTTP
	// to HTTPS redirect server on. If empty, no redirect server is started.
	HTTPRedirectAddr string `mapstructure:"http_redirect_addr" yaml:"http_redirect_addr,omitempty"`

	// Timeout settings : https://github.com/pomerium/pomerium/issues/40
	ReadTimeout  time.Duration `mapstructure:"timeout_read" yaml:"timeout_read,omitempty"`
	WriteTimeout time.Duration `mapstructure:"timeout_write" yaml:"timeout_write,omitempty"`
	IdleTimeout  time.Duration `mapstructure:"timeout_idle" yaml:"timeout_idle,omitempty"`

	// Policies define per-route configuration and access control policies.
	Policies   []Policy `mapstructure:"policy"`
	PolicyFile string   `mapstructure:"policy_file" yaml:"policy_file,omitempty"`
	Routes     []Policy `mapstructure:"routes"`

	// AdditionalPolicies are any additional policies added to the options.
	AdditionalPolicies []Policy `yaml:"-"`

	// AuthenticateURL represents the externally accessible http endpoints
	// used for authentication requests and callbacks
	AuthenticateURLString         string `mapstructure:"authenticate_service_url" yaml:"authenticate_service_url,omitempty"`
	AuthenticateInternalURLString string `mapstructure:"authenticate_internal_service_url" yaml:"authenticate_internal_service_url,omitempty"`
	// SignOutRedirectURL represents the url that  user will be redirected to after signing out.
	SignOutRedirectURLString string `mapstructure:"signout_redirect_url" yaml:"signout_redirect_url,omitempty"`

	// AuthenticateCallbackPath is the path to the HTTP endpoint that will
	// receive the response from your identity provider. The value must exactly
	// match one of the authorized redirect URIs for the OAuth 2.0 client.
	// Defaults to: `/oauth2/callback`
	AuthenticateCallbackPath string `mapstructure:"authenticate_callback_path" yaml:"authenticate_callback_path,omitempty"`

	// Session/Cookie management
	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
	CookieName       string        `mapstructure:"cookie_name" yaml:"cookie_name,omitempty"`
	CookieSecret     string        `mapstructure:"cookie_secret" yaml:"cookie_secret,omitempty"`
	CookieSecretFile string        `mapstructure:"cookie_secret_file" yaml:"cookie_secret_file,omitempty"`
	CookieDomain     string        `mapstructure:"cookie_domain" yaml:"cookie_domain,omitempty"`
	CookieSecure     bool          `mapstructure:"cookie_secure" yaml:"cookie_secure,omitempty"`
	CookieHTTPOnly   bool          `mapstructure:"cookie_http_only" yaml:"cookie_http_only,omitempty"`
	CookieExpire     time.Duration `mapstructure:"cookie_expire" yaml:"cookie_expire,omitempty"`
	CookieSameSite   string        `mapstructure:"cookie_same_site" yaml:"cookie_same_site,omitempty"`

	// Identity provider configuration variables as specified by RFC6749
	// https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
	ClientID         string   `mapstructure:"idp_client_id" yaml:"idp_client_id,omitempty"`
	ClientSecret     string   `mapstructure:"idp_client_secret" yaml:"idp_client_secret,omitempty"`
	ClientSecretFile string   `mapstructure:"idp_client_secret_file" yaml:"idp_client_secret_file,omitempty"`
	Provider         string   `mapstructure:"idp_provider" yaml:"idp_provider,omitempty"`
	ProviderURL      string   `mapstructure:"idp_provider_url" yaml:"idp_provider_url,omitempty"`
	Scopes           []string `mapstructure:"idp_scopes" yaml:"idp_scopes,omitempty"`

	// RequestParams are custom request params added to the signin request as
	// part of an Oauth2 code flow.
	//
	// https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml
	// https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
	RequestParams map[string]string `mapstructure:"idp_request_params" yaml:"idp_request_params,omitempty"`

	// AuthorizeURLString is the routable destination of the authorize service's
	// gRPC endpoint. NOTE: As many load balancers do not support
	// externally routed gRPC so this may be an internal location.
	AuthorizeURLString         string   `mapstructure:"authorize_service_url" yaml:"authorize_service_url,omitempty"`
	AuthorizeURLStrings        []string `mapstructure:"authorize_service_urls" yaml:"authorize_service_urls,omitempty"`
	AuthorizeInternalURLString string   `mapstructure:"authorize_internal_service_url" yaml:"authorize_internal_service_url,omitempty"`

	// Settings to enable custom behind-the-ingress service communication
	OverrideCertificateName string `mapstructure:"override_certificate_name" yaml:"override_certificate_name,omitempty"`
	CA                      string `mapstructure:"certificate_authority" yaml:"certificate_authority,omitempty"`
	CAFile                  string `mapstructure:"certificate_authority_file" yaml:"certificate_authority_file,omitempty"`

	// DeriveInternalDomainCert is an option that would derive certificate authority
	// and domain certificates from the shared key and use them for internal communication
	DeriveInternalDomainCert *string `mapstructure:"tls_derive" yaml:"tls_derive,omitempty"`

	// SigningKey is the private key used to add a JWT-signature to upstream requests.
	// https://www.pomerium.com/docs/topics/getting-users-identity.html
	SigningKey     string `mapstructure:"signing_key" yaml:"signing_key,omitempty"`
	SigningKeyFile string `mapstructure:"signing_key_file" yaml:"signing_key_file,omitempty"`

	HeadersEnv string `yaml:",omitempty"`
	// SetResponseHeaders to set on all proxied requests. Add a 'disable' key map to turn off.
	SetResponseHeaders map[string]string `yaml:",omitempty"`

	// List of JWT claims to insert as x-pomerium-claim-* headers on proxied requests
	JWTClaimsHeaders JWTClaimHeaders `mapstructure:"jwt_claims_headers" yaml:"jwt_claims_headers,omitempty"`

	DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout" yaml:"default_upstream_timeout,omitempty"`

	// Address/Port to bind to for prometheus metrics
	MetricsAddr string `mapstructure:"metrics_address" yaml:"metrics_address,omitempty"`
	// - require basic auth for prometheus metrics, base64 encoded user:pass string
	MetricsBasicAuth string `mapstructure:"metrics_basic_auth" yaml:"metrics_basic_auth,omitempty"`
	// - TLS options
	MetricsCertificate        string `mapstructure:"metrics_certificate" yaml:"metrics_certificate,omitempty"`
	MetricsCertificateKey     string `mapstructure:"metrics_certificate_key" yaml:"metrics_certificate_key,omitempty"`
	MetricsCertificateFile    string `mapstructure:"metrics_certificate_file" yaml:"metrics_certificate_file,omitempty"`
	MetricsCertificateKeyFile string `mapstructure:"metrics_certificate_key_file" yaml:"metrics_certificate_key_file,omitempty"`
	MetricsClientCA           string `mapstructure:"metrics_client_ca" yaml:"metrics_client_ca,omitempty"`
	MetricsClientCAFile       string `mapstructure:"metrics_client_ca_file" yaml:"metrics_client_ca_file,omitempty"`

	// Tracing shared settings
	TracingProvider   string  `mapstructure:"tracing_provider" yaml:"tracing_provider,omitempty"`
	TracingSampleRate float64 `mapstructure:"tracing_sample_rate" yaml:"tracing_sample_rate,omitempty"`

	// Datadog tracing address
	TracingDatadogAddress string `mapstructure:"tracing_datadog_address" yaml:"tracing_datadog_address,omitempty"`

	//  Jaeger
	//
	// CollectorEndpoint is the full url to the Jaeger HTTP Thrift collector.
	// For example, http://localhost:14268/api/traces
	TracingJaegerCollectorEndpoint string `mapstructure:"tracing_jaeger_collector_endpoint" yaml:"tracing_jaeger_collector_endpoint,omitempty"`
	// AgentEndpoint instructs exporter to send spans to jaeger-agent at this address.
	// For example, localhost:6831.
	TracingJaegerAgentEndpoint string `mapstructure:"tracing_jaeger_agent_endpoint" yaml:"tracing_jaeger_agent_endpoint,omitempty"`

	// Zipkin
	//
	// ZipkinEndpoint configures the zipkin collector URI
	// Example: http://zipkin:9411/api/v2/spans
	ZipkinEndpoint string `mapstructure:"tracing_zipkin_endpoint" yaml:"tracing_zipkin_endpoint"`

	// GRPC Service Settings

	// GRPCAddr specifies the host and port on which the server should serve
	// gRPC requests. If running in all-in-one mode, ":5443" (localhost:5443) is used.
	GRPCAddr string `mapstructure:"grpc_address" yaml:"grpc_address,omitempty"`

	// GRPCInsecure disables transport security.
	// If running in all-in-one mode, defaults to true.
	GRPCInsecure *bool `mapstructure:"grpc_insecure" yaml:"grpc_insecure,omitempty"`

	GRPCClientTimeout       time.Duration `mapstructure:"grpc_client_timeout" yaml:"grpc_client_timeout,omitempty"`
	GRPCClientDNSRoundRobin bool          `mapstructure:"grpc_client_dns_roundrobin" yaml:"grpc_client_dns_roundrobin,omitempty"`

	// DataBrokerURLString is the routable destination of the databroker service's gRPC endpoint.
	DataBrokerURLString         string   `mapstructure:"databroker_service_url" yaml:"databroker_service_url,omitempty"`
	DataBrokerURLStrings        []string `mapstructure:"databroker_service_urls" yaml:"databroker_service_urls,omitempty"`
	DataBrokerInternalURLString string   `mapstructure:"databroker_internal_service_url" yaml:"databroker_internal_service_url,omitempty"`
	// DataBrokerStorageType is the storage backend type that databroker will use.
	// Supported type: memory, postgres
	DataBrokerStorageType string `mapstructure:"databroker_storage_type" yaml:"databroker_storage_type,omitempty"`
	// DataBrokerStorageConnectionString is the data source name for storage backend.
	DataBrokerStorageConnectionString string `mapstructure:"databroker_storage_connection_string" yaml:"databroker_storage_connection_string,omitempty"`
	DataBrokerStorageCertFile         string `mapstructure:"databroker_storage_cert_file" yaml:"databroker_storage_cert_file,omitempty"`
	DataBrokerStorageCertKeyFile      string `mapstructure:"databroker_storage_key_file" yaml:"databroker_storage_key_file,omitempty"`
	DataBrokerStorageCAFile           string `mapstructure:"databroker_storage_ca_file" yaml:"databroker_storage_ca_file,omitempty"`
	DataBrokerStorageCertSkipVerify   bool   `mapstructure:"databroker_storage_tls_skip_verify" yaml:"databroker_storage_tls_skip_verify,omitempty"`

	// ClientCA is the base64-encoded certificate authority to validate client mTLS certificates against.
	//
	// Deprecated: Use DownstreamMTLS.CA instead.
	ClientCA string `mapstructure:"client_ca" yaml:"client_ca,omitempty"`
	// ClientCAFile points to a file that contains the certificate authority to validate client mTLS certificates against.
	//
	// Deprecated: Use DownstreamMTLS.CAFile instead.
	ClientCAFile string `mapstructure:"client_ca_file" yaml:"client_ca_file,omitempty"`

	// DownstreamMTLS holds all downstream mTLS settings.
	DownstreamMTLS DownstreamMTLSSettings `mapstructure:"downstream_mtls" yaml:"downstream_mtls,omitempty"`

	// GoogleCloudServerlessAuthenticationServiceAccount is the service account to use for GCP serverless authentication.
	// If unset, the GCP metadata server will be used to query for identity tokens.
	GoogleCloudServerlessAuthenticationServiceAccount string `mapstructure:"google_cloud_serverless_authentication_service_account" yaml:"google_cloud_serverless_authentication_service_account,omitempty"`

	// UseProxyProtocol configures the HTTP listener to require the HAProxy proxy protocol (either v1 or v2) on incoming requests.
	UseProxyProtocol bool `mapstructure:"use_proxy_protocol" yaml:"use_proxy_protocol,omitempty" json:"use_proxy_protocol,omitempty"`

	viper *viper.Viper

	AutocertOptions `mapstructure:",squash" yaml:",inline"`

	// SkipXffAppend instructs proxy not to append its IP address to x-forwarded-for header.
	// see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers.html?highlight=skip_xff_append#x-forwarded-for
	SkipXffAppend bool `mapstructure:"skip_xff_append" yaml:"skip_xff_append,omitempty" json:"skip_xff_append,omitempty"`
	// XffNumTrustedHops determines the trusted client address from x-forwarded-for addresses.
	// see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers.html?highlight=xff_num_trusted_hops#x-forwarded-for
	XffNumTrustedHops uint32 `mapstructure:"xff_num_trusted_hops" yaml:"xff_num_trusted_hops,omitempty" json:"xff_num_trusted_hops,omitempty"`

	// Envoy bootstrap options. These do not support dynamic updates.
	EnvoyAdminAccessLogPath      string    `mapstructure:"envoy_admin_access_log_path" yaml:"envoy_admin_access_log_path"`
	EnvoyAdminProfilePath        string    `mapstructure:"envoy_admin_profile_path" yaml:"envoy_admin_profile_path"`
	EnvoyAdminAddress            string    `mapstructure:"envoy_admin_address" yaml:"envoy_admin_address"`
	EnvoyBindConfigSourceAddress string    `mapstructure:"envoy_bind_config_source_address" yaml:"envoy_bind_config_source_address,omitempty"`
	EnvoyBindConfigFreebind      null.Bool `mapstructure:"envoy_bind_config_freebind" yaml:"envoy_bind_config_freebind,omitempty"`

	// ProgrammaticRedirectDomainWhitelist restricts the allowed redirect URLs when using programmatic login.
	ProgrammaticRedirectDomainWhitelist []string `mapstructure:"programmatic_redirect_domain_whitelist" yaml:"programmatic_redirect_domain_whitelist,omitempty" json:"programmatic_redirect_domain_whitelist,omitempty"`

	// CodecType is the codec to use for downstream connections.
	CodecType CodecType `mapstructure:"codec_type" yaml:"codec_type"`

	AuditKey *PublicKeyEncryptionKeyOptions `mapstructure:"audit_key"`

	BrandingOptions httputil.BrandingOptions

	PassIdentityHeaders *bool `mapstructure:"pass_identity_headers" yaml:"pass_identity_headers"`
}

type certificateFilePair struct {
	// CertFile and KeyFile is the x509 certificate used to hydrate TLSCertificate
	CertFile string `mapstructure:"cert" yaml:"cert,omitempty"`
	KeyFile  string `mapstructure:"key" yaml:"key,omitempty"`
}

// DefaultOptions are the default configuration options for pomerium
var defaultOptions = Options{
	LogLevel:                 LogLevelInfo,
	Services:                 "all",
	CookieHTTPOnly:           true,
	CookieSecure:             true,
	CookieExpire:             14 * time.Hour,
	CookieName:               "_pomerium",
	DefaultUpstreamTimeout:   30 * time.Second,
	Addr:                     ":443",
	ReadTimeout:              30 * time.Second,
	WriteTimeout:             0, // support streaming by default
	IdleTimeout:              5 * time.Minute,
	GRPCAddr:                 ":443",
	GRPCClientTimeout:        10 * time.Second, // Try to withstand transient service failures for a single request
	GRPCClientDNSRoundRobin:  true,
	AuthenticateCallbackPath: "/oauth2/callback",
	TracingSampleRate:        0.0001,

	AutocertOptions: AutocertOptions{
		Folder: dataDir(),
	},
	DataBrokerStorageType:               "memory",
	SkipXffAppend:                       false,
	XffNumTrustedHops:                   0,
	EnvoyAdminAccessLogPath:             os.DevNull,
	EnvoyAdminProfilePath:               os.DevNull,
	ProgrammaticRedirectDomainWhitelist: []string{"localhost"},
}

var defaultSetResponseHeaders = map[string]string{
	"X-Frame-Options":           "SAMEORIGIN",
	"X-XSS-Protection":          "1; mode=block",
	"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
}

// NewDefaultOptions returns a copy the default options. It's the caller's
// responsibility to do a follow up Validate call.
func NewDefaultOptions() *Options {
	newOpts := defaultOptions
	newOpts.viper = viper.New()
	return &newOpts
}

// newOptionsFromConfig builds the main binary's configuration options by parsing
// environmental variables and config file
func newOptionsFromConfig(configFile string) (*Options, error) {
	o, err := optionsFromViper(configFile)
	if err != nil {
		return nil, fmt.Errorf("config: options from config file %q: %w", configFile, err)
	}
	serviceName := telemetry.ServiceName(o.Services)
	metrics.AddPolicyCountCallback(serviceName, func() int64 {
		return int64(len(o.GetAllPolicies()))
	})

	return o, nil
}

func optionsFromViper(configFile string) (*Options, error) {
	// start a copy of the default options
	o := NewDefaultOptions()
	v := o.viper
	// Load up config
	err := bindEnvs(v)
	if err != nil {
		return nil, fmt.Errorf("failed to bind options to env vars: %w", err)
	}

	if configFile != "" {
		v.SetConfigFile(configFile)
		if err := v.ReadInConfig(); err != nil {
			return nil, fmt.Errorf("failed to read config: %w", err)
		}
	}

	var metadata mapstructure.Metadata
	if err := v.Unmarshal(o, ViperPolicyHooks, func(c *mapstructure.DecoderConfig) { c.Metadata = &metadata }); err != nil {
		return nil, fmt.Errorf("failed to unmarshal config: %w", err)
	}
	if err := checkConfigKeysErrors(configFile, metadata.Unused); err != nil {
		return nil, err
	}

	// This is necessary because v.Unmarshal will overwrite .viper field.
	o.viper = v

	if err := o.Validate(); err != nil {
		return nil, fmt.Errorf("validation error %w", err)
	}
	return o, nil
}

func checkConfigKeysErrors(configFile string, unused []string) error {
	checks := CheckUnknownConfigFields(unused)
	ctx := context.Background()
	errInvalidConfigKeys := errors.New("some configuration options are no longer supported, please check logs for details")
	var err error

	for _, check := range checks {
		var evt *zerolog.Event
		switch check.KeyAction {
		case KeyActionError:
			evt = log.Error(ctx)
			err = errInvalidConfigKeys
		default:
			evt = log.Warn(ctx)
		}
		evt.Str("config_file", configFile).Str("key", check.Key)
		if check.DocsURL != "" {
			evt = evt.Str("help", check.DocsURL)
		}
		evt.Msg(string(check.FieldCheckMsg))
	}
	return err
}

// parsePolicy initializes policy to the options from either base64 environmental
// variables or from a file
func (o *Options) parsePolicy() error {
	var policies []Policy
	if err := o.viper.UnmarshalKey("policy", &policies, ViperPolicyHooks); err != nil {
		return err
	}
	if len(policies) != 0 {
		o.Policies = policies
	}

	var routes []Policy
	if err := o.viper.UnmarshalKey("routes", &routes, ViperPolicyHooks); err != nil {
		return err
	}
	if len(routes) != 0 {
		o.Routes = routes
	}

	// Finish initializing policies
	for i := range o.Policies {
		p := &o.Policies[i]
		if err := p.Validate(); err != nil {
			return err
		}
	}
	for i := range o.Routes {
		p := &o.Routes[i]
		if err := p.Validate(); err != nil {
			return err
		}
	}
	for i := range o.AdditionalPolicies {
		p := o.AdditionalPolicies[i]
		if err := p.Validate(); err != nil {
			return err
		}
	}
	return nil
}

func (o *Options) viperSet(key string, value interface{}) {
	o.viper.Set(key, value)
}

func (o *Options) viperIsSet(key string) bool {
	return o.viper.IsSet(key)
}

// parseHeaders handles unmarshalling any custom headers correctly from the
// environment or viper's parsed keys
func (o *Options) parseHeaders(_ context.Context) error {
	var headers map[string]string
	if o.HeadersEnv != "" {
		// Handle JSON by default via viper
		if headers = o.viper.GetStringMapString("HeadersEnv"); len(headers) == 0 {
			// Try to parse "Key1:Value1,Key2:Value2" syntax
			headerSlice := strings.Split(o.HeadersEnv, ",")
			for n := range headerSlice {
				headerFields := strings.SplitN(headerSlice[n], ":", 2)
				if len(headerFields) == 2 {
					headers[headerFields[0]] = headerFields[1]
				} else {
					// Something went wrong
					return fmt.Errorf("failed to decode headers from '%s'", o.HeadersEnv)
				}
			}

		}
		o.SetResponseHeaders = headers
		return nil
	}

	if o.viperIsSet("set_response_headers") {
		if err := o.viper.UnmarshalKey("set_response_headers", &headers); err != nil {
			return fmt.Errorf("header %s failed to parse: %w", o.viper.Get("set_response_headers"), err)
		}
		o.SetResponseHeaders = headers
	}
	return nil
}

// bindEnvs adds a Viper environment variable binding for each field in the
// Options struct (including nested structs), based on the mapstructure tag.
func bindEnvs(v *viper.Viper) error {
	if _, err := bindEnvsRecursive(reflect.TypeOf(Options{}), v, "", ""); err != nil {
		return err
	}

	// Statically bind fields
	err := v.BindEnv("Policy", "POLICY")
	if err != nil {
		return fmt.Errorf("failed to bind field 'Policy' to env var 'POLICY': %w", err)
	}
	err = v.BindEnv("HeadersEnv", "HEADERS")
	if err != nil {
		return fmt.Errorf("failed to bind field 'HeadersEnv' to env var 'HEADERS': %w", err)
	}

	return nil
}

// bindEnvsRecursive binds all fields of the provided struct type that have a
// "mapstructure" tag to corresponding environment variables, recursively. If a
// nested struct contains no fields with a "mapstructure" tag, a binding will
// be added for the struct itself (e.g. null.Bool).
func bindEnvsRecursive(t reflect.Type, v *viper.Viper, keyPrefix, envPrefix string) (bool, error) {
	anyFieldHasMapstructureTag := false
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		tag, hasTag := field.Tag.Lookup("mapstructure")
		if !hasTag || tag == "-" {
			continue
		}

		anyFieldHasMapstructureTag = true

		key, _, _ := strings.Cut(tag, ",")
		keyPath := keyPrefix + key
		envName := envPrefix + strings.ToUpper(key)

		if field.Type.Kind() == reflect.Struct {
			newKeyPrefix := keyPath
			newEnvPrefix := envName
			if key != "" {
				newKeyPrefix += "."
				newEnvPrefix += "_"
			}
			nestedMapstructure, err := bindEnvsRecursive(field.Type, v, newKeyPrefix, newEnvPrefix)
			if err != nil {
				return false, err
			} else if nestedMapstructure {
				// If we've bound any nested fields from this struct, do not
				// also bind this struct itself.
				continue
			}
		}

		if key != "" {
			if err := v.BindEnv(keyPath, envName); err != nil {
				return false, fmt.Errorf("failed to bind field '%s' to env var '%s': %w",
					field.Name, envName, err)
			}
		}
	}
	return anyFieldHasMapstructureTag, nil
}

// Validate ensures the Options fields are valid, and hydrated.
func (o *Options) Validate() error {
	ctx := context.TODO()
	if !IsValidService(o.Services) {
		return fmt.Errorf("config: %s is an invalid service type", o.Services)
	}

	switch o.DataBrokerStorageType {
	case StorageInMemoryName:
	case StoragePostgresName:
		if o.DataBrokerStorageConnectionString == "" {
			return errors.New("config: missing databroker storage backend dsn")
		}
	default:
		return errors.New("config: unknown databroker storage backend type")
	}

	_, err := o.GetSharedKey()
	if err != nil {
		return fmt.Errorf("config: invalid shared secret: %w", err)
	}

	if o.AuthenticateURLString != "" {
		_, err := urlutil.ParseAndValidateURL(o.AuthenticateURLString)
		if err != nil {
			return fmt.Errorf("config: bad authenticate-url %s : %w", o.AuthenticateURLString, err)
		}
	}
	if o.AuthenticateInternalURLString != "" {
		_, err := urlutil.ParseAndValidateURL(o.AuthenticateInternalURLString)
		if err != nil {
			return fmt.Errorf("config: bad authenticate-internal-url %s : %w", o.AuthenticateInternalURLString, err)
		}
	}

	if o.SignOutRedirectURLString != "" {
		_, err := urlutil.ParseAndValidateURL(o.SignOutRedirectURLString)
		if err != nil {
			return fmt.Errorf("config: bad signout-redirect-url %s : %w", o.SignOutRedirectURLString, err)
		}
	}

	if o.AuthorizeURLString != "" {
		_, err := urlutil.ParseAndValidateURL(o.AuthorizeURLString)
		if err != nil {
			return fmt.Errorf("config: bad authorize-url %s : %w", o.AuthorizeURLString, err)
		}
	}
	if o.AuthorizeInternalURLString != "" {
		_, err := urlutil.ParseAndValidateURL(o.AuthorizeInternalURLString)
		if err != nil {
			return fmt.Errorf("config: bad authorize-internal-url %s : %w", o.AuthorizeInternalURLString, err)
		}
	}

	if o.DataBrokerURLString != "" {
		_, err := urlutil.ParseAndValidateURL(o.DataBrokerURLString)
		if err != nil {
			return fmt.Errorf("config: bad databroker service url %s : %w", o.DataBrokerURLString, err)
		}
	}
	if o.DataBrokerInternalURLString != "" {
		_, err := urlutil.ParseAndValidateURL(o.DataBrokerInternalURLString)
		if err != nil {
			return fmt.Errorf("config: bad databroker internal service url %s : %w", o.DataBrokerInternalURLString, err)
		}
	}

	if o.PolicyFile != "" {
		return errors.New("config: policy file setting is deprecated")
	}
	if err := o.parsePolicy(); err != nil {
		return fmt.Errorf("config: failed to parse policy: %w", err)
	}

	if err := o.parseHeaders(ctx); err != nil {
		return fmt.Errorf("config: failed to parse headers: %w", err)
	}

	hasCert := false

	if o.Cert != "" || o.Key != "" {
		_, err := cryptutil.CertificateFromBase64(o.Cert, o.Key)
		if err != nil {
			return fmt.Errorf("config: bad cert base64 %w", err)
		}
		hasCert = true
	}

	for _, c := range o.CertificateData {
		_, err := tls.X509KeyPair(c.GetCertBytes(), c.GetKeyBytes())
		if err != nil {
			return fmt.Errorf("config: bad cert entry, cert is invalid: %w", err)
		}
		hasCert = true
	}

	for _, c := range o.CertificateFiles {
		_, err := cryptutil.CertificateFromFile(c.CertFile, c.KeyFile)
		if err != nil {
			return fmt.Errorf("config: bad cert entry, file reference invalid. %w", err)
		}
		hasCert = true
	}

	if o.CertFile != "" || o.KeyFile != "" {
		_, err := cryptutil.CertificateFromFile(o.CertFile, o.KeyFile)
		if err != nil {
			return fmt.Errorf("config: bad cert file %w", err)
		}
		hasCert = true
	}

	if o.DataBrokerStorageCertFile != "" || o.DataBrokerStorageCertKeyFile != "" {
		_, err := cryptutil.CertificateFromFile(o.DataBrokerStorageCertFile, o.DataBrokerStorageCertKeyFile)
		if err != nil {
			return fmt.Errorf("config: bad databroker cert file %w", err)
		}
	}

	if o.DataBrokerStorageCAFile != "" {
		if _, err := os.Stat(o.DataBrokerStorageCAFile); err != nil {
			return fmt.Errorf("config: bad databroker ca file: %w", err)
		}
	}

	if o.ClientCA != "" {
		log.Warn(context.Background()).Msg("config: client_ca is deprecated, set " +
			"downstream_mtls.ca instead")
		if o.DownstreamMTLS.CA == "" {
			o.DownstreamMTLS.CA = o.ClientCA
		}
	}
	if o.ClientCAFile != "" {
		log.Warn(context.Background()).Msg("config: client_ca_file is deprecated, set " +
			"downstream_mtls.ca_file instead")
		if o.DownstreamMTLS.CAFile == "" {
			o.DownstreamMTLS.CAFile = o.ClientCAFile
		}
	}

	if err := o.DownstreamMTLS.validate(); err != nil {
		return fmt.Errorf("config: bad downstream mTLS settings: %w", err)
	}

	// strip quotes from redirect address (#811)
	o.HTTPRedirectAddr = strings.Trim(o.HTTPRedirectAddr, `"'`)

	if !o.InsecureServer && !hasCert && !o.AutocertOptions.Enable {
		log.Warn(ctx).Msg("neither `autocert`, " +
			"`insecure_server` or manually provided certificates were provided, server will be using a self-signed certificate")
	}

	if err := ValidateDNSLookupFamily(o.DNSLookupFamily); err != nil {
		return fmt.Errorf("config: %w", err)
	}

	if o.MetricsAddr != "" {
		if err := ValidateMetricsAddress(o.MetricsAddr); err != nil {
			return fmt.Errorf("config: invalid metrics_addr: %w", err)
		}
	}

	// validate metrics basic auth
	if o.MetricsBasicAuth != "" {
		str, err := base64.StdEncoding.DecodeString(o.MetricsBasicAuth)
		if err != nil {
			return fmt.Errorf("config: metrics_basic_auth must be a base64 encoded string")
		}

		if !strings.Contains(string(str), ":") {
			return fmt.Errorf("config: metrics_basic_auth should contain a user name and password separated by a colon")
		}
	}

	if o.MetricsCertificate != "" && o.MetricsCertificateKey != "" {
		_, err := cryptutil.CertificateFromBase64(o.MetricsCertificate, o.MetricsCertificateKey)
		if err != nil {
			return fmt.Errorf("config: invalid metrics_certificate or metrics_certificate_key: %w", err)
		}
	}

	if o.MetricsCertificateFile != "" && o.MetricsCertificateKeyFile != "" {
		_, err := cryptutil.CertificateFromFile(o.MetricsCertificateFile, o.MetricsCertificateKeyFile)
		if err != nil {
			return fmt.Errorf("config: invalid metrics_certificate_file or metrics_certificate_key_file: %w", err)
		}
	}

	// validate the Autocert options
	err = o.AutocertOptions.Validate()
	if err != nil {
		return err
	}

	if err := ValidateCookieSameSite(o.CookieSameSite); err != nil {
		return fmt.Errorf("config: invalid cookie_same_site: %w", err)
	} else if !o.CookieSecure && o.GetCookieSameSite() == http.SameSiteNoneMode {
		return errors.New("config: cannot use cookie_same_site: none with cookie_secure: false")
	}

	if err := ValidateLogLevel(o.LogLevel); err != nil {
		return fmt.Errorf("config: invalid log_level: %w", err)
	}

	if err := ValidateLogLevel(o.ProxyLogLevel); err != nil {
		return fmt.Errorf("config: invalid proxy_log_level: %w", err)
	}

	for _, field := range o.AccessLogFields {
		if err := field.Validate(); err != nil {
			return fmt.Errorf("config: invalid access_log_fields: %w", err)
		}
	}

	for _, field := range o.AuthorizeLogFields {
		if err := field.Validate(); err != nil {
			return fmt.Errorf("config: invalid authorize_log_fields: %w", err)
		}
	}

	return nil
}

// GetDeriveInternalDomain returns an optional internal domain name to use for gRPC endpoint
func (o *Options) GetDeriveInternalDomain() string {
	if o.DeriveInternalDomainCert == nil {
		return ""
	}
	return strings.ToLower(*o.DeriveInternalDomainCert)
}

// GetAuthenticateURL returns the AuthenticateURL in the options or 127.0.0.1.
func (o *Options) GetAuthenticateURL() (*url.URL, error) {
	rawURL := o.AuthenticateURLString
	if rawURL == "" {
		rawURL = "https://authenticate.pomerium.app"
	}
	return urlutil.ParseAndValidateURL(rawURL)
}

// GetInternalAuthenticateURL returns the internal AuthenticateURL in the options or the AuthenticateURL.
func (o *Options) GetInternalAuthenticateURL() (*url.URL, error) {
	rawURL := o.AuthenticateInternalURLString
	if rawURL == "" {
		return o.GetAuthenticateURL()
	}
	return urlutil.ParseAndValidateURL(o.AuthenticateInternalURLString)
}

// UseStatelessAuthenticateFlow returns true if the stateless authentication
// flow should be used (i.e. for hosted authenticate).
func (o *Options) UseStatelessAuthenticateFlow() bool {
	if flow := os.Getenv("DEBUG_FORCE_AUTHENTICATE_FLOW"); flow != "" {
		if flow == "stateless" {
			return true
		} else if flow == "stateful" {
			return false
		}
		log.Warn(context.Background()).
			Msgf("ignoring unknown DEBUG_FORCE_AUTHENTICATE_FLOW setting %q", flow)
	}
	u, err := o.GetInternalAuthenticateURL()
	if err != nil {
		return false
	}
	return urlutil.IsHostedAuthenticateDomain(u.Hostname())
}

// GetAuthorizeURLs returns the AuthorizeURLs in the options or 127.0.0.1:5443.
func (o *Options) GetAuthorizeURLs() ([]*url.URL, error) {
	if IsAll(o.Services) && o.AuthorizeURLString == "" && len(o.AuthorizeURLStrings) == 0 {
		u, err := urlutil.ParseAndValidateURL("http://127.0.0.1" + DefaultAlternativeAddr)
		if err != nil {
			return nil, err
		}
		return []*url.URL{u}, nil
	}
	return o.getURLs(append([]string{o.AuthorizeURLString}, o.AuthorizeURLStrings...)...)
}

// GetInternalAuthorizeURLs returns the internal AuthorizeURLs in the options or the AuthorizeURLs.
func (o *Options) GetInternalAuthorizeURLs() ([]*url.URL, error) {
	rawurl := o.AuthorizeInternalURLString
	if rawurl == "" {
		return o.GetAuthorizeURLs()
	}
	return o.getURLs(rawurl)
}

// GetDataBrokerURLs returns the DataBrokerURLs in the options or 127.0.0.1:5443.
func (o *Options) GetDataBrokerURLs() ([]*url.URL, error) {
	if IsAll(o.Services) && o.DataBrokerURLString == "" && len(o.DataBrokerURLStrings) == 0 {
		u, err := urlutil.ParseAndValidateURL("http://127.0.0.1" + DefaultAlternativeAddr)
		if err != nil {
			return nil, err
		}
		return []*url.URL{u}, nil
	}
	return o.getURLs(append([]string{o.DataBrokerURLString}, o.DataBrokerURLStrings...)...)
}

// GetInternalDataBrokerURLs returns the internal DataBrokerURLs in the options or the DataBrokerURLs.
func (o *Options) GetInternalDataBrokerURLs() ([]*url.URL, error) {
	rawurl := o.DataBrokerInternalURLString
	if rawurl == "" {
		return o.GetDataBrokerURLs()
	}
	return o.getURLs(rawurl)
}

func (o *Options) getURLs(strs ...string) ([]*url.URL, error) {
	var urls []*url.URL
	if o != nil {
		for _, str := range strs {
			if str == "" {
				continue
			}
			u, err := urlutil.ParseAndValidateURL(str)
			if err != nil {
				return nil, err
			}
			urls = append(urls, u)
		}
	}
	if len(urls) == 0 {
		u, _ := url.Parse("http://127.0.0.1" + DefaultAlternativeAddr)
		urls = append(urls, u)
	}
	return urls, nil
}

// GetGRPCAddr gets the gRPC address.
func (o *Options) GetGRPCAddr() string {
	// to avoid port collision when running on localhost
	if IsAll(o.Services) && o.GRPCAddr == defaultOptions.GRPCAddr {
		return DefaultAlternativeAddr
	}
	return o.GRPCAddr
}

// GetGRPCInsecure gets whether or not gRPC is insecure.
func (o *Options) GetGRPCInsecure() bool {
	if o.GRPCInsecure != nil {
		return *o.GRPCInsecure
	}
	if IsAll(o.Services) {
		return true
	}
	return false
}

// GetSignOutRedirectURL gets the SignOutRedirectURL.
func (o *Options) GetSignOutRedirectURL() (*url.URL, error) {
	rawurl := o.SignOutRedirectURLString
	if rawurl == "" {
		return nil, nil
	}
	return urlutil.ParseAndValidateURL(rawurl)
}

// GetMetricsCertificate returns the metrics certificate to use for TLS. `nil` will be
// returned if there is no certificate.
func (o *Options) GetMetricsCertificate() (*tls.Certificate, error) {
	if o.MetricsCertificate != "" && o.MetricsCertificateKey != "" {
		return cryptutil.CertificateFromBase64(o.MetricsCertificate, o.MetricsCertificateKey)
	}
	if o.MetricsCertificateFile != "" && o.MetricsCertificateKeyFile != "" {
		return cryptutil.CertificateFromFile(o.MetricsCertificateFile, o.MetricsCertificateKeyFile)
	}
	return nil, nil
}

// GetOauthOptions gets the oauth.Options for the given config options.
func (o *Options) GetOauthOptions() (oauth.Options, error) {
	redirectURL, err := o.GetAuthenticateURL()
	if err != nil {
		return oauth.Options{}, err
	}
	redirectURL = redirectURL.ResolveReference(&url.URL{
		Path: o.AuthenticateCallbackPath,
	})
	clientSecret, err := o.GetClientSecret()
	if err != nil {
		return oauth.Options{}, err
	}
	return oauth.Options{
		RedirectURL:  redirectURL,
		ProviderName: o.Provider,
		ProviderURL:  o.ProviderURL,
		ClientID:     o.ClientID,
		ClientSecret: clientSecret,
		Scopes:       o.Scopes,
	}, nil
}

// GetAllPolicies gets all the policies in the options.
func (o *Options) GetAllPolicies() []Policy {
	if o == nil {
		return nil
	}
	policies := make([]Policy, 0, len(o.Policies)+len(o.Routes)+len(o.AdditionalPolicies))
	policies = append(policies, o.Policies...)
	policies = append(policies, o.Routes...)
	policies = append(policies, o.AdditionalPolicies...)
	return policies
}

// GetMetricsBasicAuth gets the metrics basic auth username and password.
func (o *Options) GetMetricsBasicAuth() (username, password string, ok bool) {
	if o.MetricsBasicAuth == "" {
		return "", "", false
	}

	bs, err := base64.StdEncoding.DecodeString(o.MetricsBasicAuth)
	if err != nil {
		return "", "", false
	}

	idx := bytes.Index(bs, []byte{':'})
	if idx == -1 {
		return "", "", false
	}

	return string(bs[:idx]), string(bs[idx+1:]), true
}

// HasAnyDownstreamMTLSClientCA returns true if there is a global downstream
// client CA or there are any per-route downstream client CAs.
func (o *Options) HasAnyDownstreamMTLSClientCA() bool {
	// All the CA settings should already have been validated.
	ca, _ := o.DownstreamMTLS.GetCA()
	if len(ca) > 0 {
		return true
	}
	allPolicies := o.GetAllPolicies()
	for i := range allPolicies {
		// We don't need to check TLSDownstreamClientCAFile here because
		// Policy.Validate() will populate TLSDownstreamClientCA when
		// TLSDownstreamClientCAFile is set.
		if allPolicies[i].TLSDownstreamClientCA != "" {
			return true
		}
	}
	return false
}

// GetDataBrokerCertificate gets the optional databroker certificate. This method will return nil if no certificate is
// specified.
func (o *Options) GetDataBrokerCertificate() (*tls.Certificate, error) {
	if o.DataBrokerStorageCertFile == "" || o.DataBrokerStorageCertKeyFile == "" {
		return nil, nil
	}
	return cryptutil.CertificateFromFile(o.DataBrokerStorageCertFile, o.DataBrokerStorageCertKeyFile)
}

// GetCertificates gets all the certificates from the options.
func (o *Options) GetCertificates() ([]tls.Certificate, error) {
	var certs []tls.Certificate
	if o.Cert != "" && o.Key != "" {
		cert, err := cryptutil.CertificateFromBase64(o.Cert, o.Key)
		if err != nil {
			return nil, fmt.Errorf("config: invalid base64 certificate: %w", err)
		}
		certs = append(certs, *cert)
	}
	for _, c := range o.CertificateFiles {
		cert, err := cryptutil.CertificateFromFile(c.CertFile, c.KeyFile)
		if err != nil {
			return nil, fmt.Errorf("config: invalid certificate entry: %w", err)
		}
		certs = append(certs, *cert)
	}
	for _, c := range o.CertificateData {
		cert, err := tls.X509KeyPair(c.GetCertBytes(), c.GetKeyBytes())
		if err != nil {
			return nil, fmt.Errorf("config: invalid certificate entry: %w", err)
		}
		certs = append(certs, cert)
	}
	if o.CertFile != "" && o.KeyFile != "" {
		cert, err := cryptutil.CertificateFromFile(o.CertFile, o.KeyFile)
		if err != nil {
			return nil, fmt.Errorf("config: bad cert file %w", err)
		}
		certs = append(certs, *cert)
	}
	return certs, nil
}

// HasCertificates returns true if options has any certificates.
func (o *Options) HasCertificates() bool {
	return o.Cert != "" ||
		o.Key != "" ||
		len(o.CertificateFiles) > 0 ||
		o.CertFile != "" ||
		o.KeyFile != "" ||
		len(o.CertificateData) > 0
}

// GetX509Certificates gets all the x509 certificates from the options. Invalid certificates are ignored.
func (o *Options) GetX509Certificates() []*x509.Certificate {
	var certs []*x509.Certificate

	if o.CertFile != "" {
		cert, err := cryptutil.ParsePEMCertificateFromFile(o.CertFile)
		if err != nil {
			log.Error(context.Background()).Err(err).Str("file", o.CertFile).Msg("invalid cert_file")
		} else {
			certs = append(certs, cert)
		}
	} else if o.Cert != "" {
		if cert, err := cryptutil.ParsePEMCertificateFromBase64(o.Cert); err != nil {
			log.Error(context.Background()).Err(err).Msg("invalid cert")
		} else {
			certs = append(certs, cert)
		}
	}

	for _, c := range o.CertificateData {
		cert, err := cryptutil.ParsePEMCertificate(c.GetCertBytes())
		if err != nil {
			log.Error(context.Background()).Err(err).Msg("invalid certificate")
		} else {
			certs = append(certs, cert)
		}
	}

	for _, c := range o.CertificateFiles {
		cert, err := cryptutil.ParsePEMCertificateFromFile(c.CertFile)
		if err != nil {
			log.Error(context.Background()).Err(err).Msg("invalid certificate_file")
		} else {
			certs = append(certs, cert)
		}
	}

	return certs
}

// GetSharedKey gets the decoded shared key.
func (o *Options) GetSharedKey() ([]byte, error) {
	sharedKey := o.SharedKey
	if o.SharedSecretFile != "" {
		bs, err := os.ReadFile(o.SharedSecretFile)
		if err != nil {
			return nil, err
		}
		sharedKey = string(bs)
	}
	// mutual auth between services on the same host can be generated at runtime
	if IsAll(o.Services) && sharedKey == "" {
		sharedKey = randomSharedKey
	}
	if sharedKey == "" {
		return nil, errors.New("empty shared secret")
	}
	if strings.TrimSpace(sharedKey) != sharedKey {
		return nil, errors.New("shared secret contains whitespace")
	}
	return base64.StdEncoding.DecodeString(sharedKey)
}

// GetHPKEPrivateKey gets the hpke.PrivateKey dervived from the shared key.
func (o *Options) GetHPKEPrivateKey() (*hpke.PrivateKey, error) {
	sharedKey, err := o.GetSharedKey()
	if err != nil {
		return nil, err
	}

	return hpke.DerivePrivateKey(sharedKey), nil
}

// GetGoogleCloudServerlessAuthenticationServiceAccount gets the GoogleCloudServerlessAuthenticationServiceAccount.
func (o *Options) GetGoogleCloudServerlessAuthenticationServiceAccount() string {
	return o.GoogleCloudServerlessAuthenticationServiceAccount
}

// GetSetResponseHeaders gets the SetResponseHeaders.
func (o *Options) GetSetResponseHeaders() map[string]string {
	return o.GetSetResponseHeadersForPolicy(nil)
}

// GetSetResponseHeadersForPolicy gets the SetResponseHeaders for a policy.
func (o *Options) GetSetResponseHeadersForPolicy(policy *Policy) map[string]string {
	hdrs := make(map[string]string)
	for k, v := range o.SetResponseHeaders {
		hdrs[k] = v
	}

	if o.SetResponseHeaders == nil {
		for k, v := range defaultSetResponseHeaders {
			hdrs[k] = v
		}

		if !o.HasCertificates() || o.AutocertOptions.UseStaging {
			delete(hdrs, "Strict-Transport-Security")
		}
	}
	if _, ok := hdrs[DisableHeaderKey]; ok {
		hdrs = make(map[string]string)
	}

	if policy != nil && policy.SetResponseHeaders != nil {
		for k, v := range policy.SetResponseHeaders {
			hdrs[k] = v
		}
	}
	if _, ok := hdrs[DisableHeaderKey]; ok {
		hdrs = make(map[string]string)
	}

	return hdrs
}

// GetCodecType gets a codec type.
func (o *Options) GetCodecType() CodecType {
	if o.CodecType == CodecTypeUnset {
		return CodecTypeAuto
	}
	return o.CodecType
}

// GetAllRouteableGRPCHosts returns all the possible gRPC hosts handled by the Pomerium options.
func (o *Options) GetAllRouteableGRPCHosts() ([]string, error) {
	hosts := sets.NewSorted[string]()

	// authorize urls
	if IsAll(o.Services) {
		authorizeURLs, err := o.GetAuthorizeURLs()
		if err != nil {
			return nil, err
		}
		for _, u := range authorizeURLs {
			hosts.Add(urlutil.GetDomainsForURL(u)...)
		}
	} else if IsAuthorize(o.Services) {
		authorizeURLs, err := o.GetInternalAuthorizeURLs()
		if err != nil {
			return nil, err
		}
		for _, u := range authorizeURLs {
			hosts.Add(urlutil.GetDomainsForURL(u)...)
		}
	}

	// databroker urls
	if IsAll(o.Services) {
		dataBrokerURLs, err := o.GetDataBrokerURLs()
		if err != nil {
			return nil, err
		}
		for _, u := range dataBrokerURLs {
			hosts.Add(urlutil.GetDomainsForURL(u)...)
		}
	} else if IsDataBroker(o.Services) {
		dataBrokerURLs, err := o.GetInternalDataBrokerURLs()
		if err != nil {
			return nil, err
		}
		for _, u := range dataBrokerURLs {
			hosts.Add(urlutil.GetDomainsForURL(u)...)
		}
	}

	return hosts.ToSlice(), nil
}

// GetAllRouteableHTTPHosts returns all the possible HTTP hosts handled by the Pomerium options.
func (o *Options) GetAllRouteableHTTPHosts() ([]string, error) {
	hosts := sets.NewSorted[string]()
	if IsAuthenticate(o.Services) {
		if o.AuthenticateInternalURLString != "" {
			authenticateURL, err := o.GetInternalAuthenticateURL()
			if err != nil {
				return nil, err
			}
			hosts.Add(urlutil.GetDomainsForURL(authenticateURL)...)
		}

		if o.AuthenticateURLString != "" {
			authenticateURL, err := o.GetAuthenticateURL()
			if err != nil {
				return nil, err
			}
			hosts.Add(urlutil.GetDomainsForURL(authenticateURL)...)
		}
	}

	// policy urls
	if IsProxy(o.Services) {
		for _, policy := range o.GetAllPolicies() {
			fromURL, err := urlutil.ParseAndValidateURL(policy.From)
			if err != nil {
				return nil, err
			}

			hosts.Add(urlutil.GetDomainsForURL(fromURL)...)
			if policy.TLSDownstreamServerName != "" {
				tlsURL := fromURL.ResolveReference(&url.URL{Host: policy.TLSDownstreamServerName})
				hosts.Add(urlutil.GetDomainsForURL(tlsURL)...)
			}
		}
	}

	return hosts.ToSlice(), nil
}

// GetClientSecret gets the client secret.
func (o *Options) GetClientSecret() (string, error) {
	if o == nil {
		return "", nil
	}
	if o.ClientSecretFile != "" {
		bs, err := os.ReadFile(o.ClientSecretFile)
		if err != nil {
			return "", err
		}
		return string(bs), nil
	}
	return o.ClientSecret, nil
}

// GetCookieSecret gets the decoded cookie secret.
func (o *Options) GetCookieSecret() ([]byte, error) {
	cookieSecret := o.CookieSecret
	if o.CookieSecretFile != "" {
		bs, err := os.ReadFile(o.CookieSecretFile)
		if err != nil {
			return nil, err
		}
		cookieSecret = string(bs)
	}

	if IsAll(o.Services) && cookieSecret == "" {
		log.WarnCookieSecret()
		cookieSecret = randomSharedKey
	}
	if cookieSecret == "" {
		return nil, errors.New("empty cookie secret")
	}

	return base64.StdEncoding.DecodeString(cookieSecret)
}

// GetCookieSameSite gets the cookie same site option.
func (o *Options) GetCookieSameSite() http.SameSite {
	str := strings.ToLower(o.CookieSameSite)
	switch str {
	case "strict":
		return http.SameSiteStrictMode
	case "lax":
		return http.SameSiteLaxMode
	case "none":
		return http.SameSiteNoneMode
	}
	return http.SameSiteDefaultMode
}

// GetCSRFSameSite gets the csrf same site option.
func (o *Options) GetCSRFSameSite() csrf.SameSiteMode {
	if o.Provider == apple.Name {
		// csrf.SameSiteLaxMode will cause browsers to reset
		// the session on POST. This breaks Appleid being able
		// to verify the csrf token.
		return csrf.SameSiteNoneMode
	}

	str := strings.ToLower(o.CookieSameSite)
	switch str {
	case "strict":
		return csrf.SameSiteStrictMode
	case "lax":
		return csrf.SameSiteLaxMode
	case "none":
		return csrf.SameSiteNoneMode
	}
	return csrf.SameSiteDefaultMode
}

// GetSigningKey gets the signing key.
func (o *Options) GetSigningKey() ([]byte, error) {
	if o == nil {
		return nil, nil
	}

	rawSigningKey := o.SigningKey
	if o.SigningKeyFile != "" {
		bs, err := os.ReadFile(o.SigningKeyFile)
		if err != nil {
			return nil, err
		}
		rawSigningKey = string(bs)
	}

	rawSigningKey = strings.TrimSpace(rawSigningKey)

	if bs, err := base64.StdEncoding.DecodeString(rawSigningKey); err == nil {
		return bs, nil
	}

	return []byte(rawSigningKey), nil
}

// GetAccessLogFields returns the access log fields. If none are set, the default fields are returned.
func (o *Options) GetAccessLogFields() []log.AccessLogField {
	if o.AccessLogFields == nil {
		return log.DefaultAccessLogFields()
	}
	return o.AccessLogFields
}

// GetAuthorizeLogFields returns the authorize log fields. If none are set, the default fields are returned.
func (o *Options) GetAuthorizeLogFields() []log.AuthorizeLogField {
	if o.AuthorizeLogFields == nil {
		return log.DefaultAuthorizeLogFields()
	}
	return o.AuthorizeLogFields
}

// NewCookie creates a new Cookie.
func (o *Options) NewCookie() *http.Cookie {
	return &http.Cookie{
		Name:     o.CookieName,
		Domain:   o.CookieDomain,
		Expires:  time.Now().Add(o.CookieExpire),
		Secure:   o.CookieSecure,
		SameSite: o.GetCookieSameSite(),
		HttpOnly: o.CookieHTTPOnly,
	}
}

// Checksum returns the checksum of the current options struct
func (o *Options) Checksum() uint64 {
	return hashutil.MustHash(o)
}

func (o *Options) applyExternalCerts(ctx context.Context, certsIndex *cryptutil.CertificatesIndex, certs []*config.Settings_Certificate) {
	for _, c := range certs {
		cert, err := cryptutil.ParsePEMCertificate(c.GetCertBytes())
		if err != nil {
			log.Error(ctx).Err(err).Msg("parsing cert from databroker: skipped")
			continue
		}

		if overlaps, name := certsIndex.OverlapsWithExistingCertificate(cert); overlaps {
			log.Error(ctx).Err(err).Str("domain", name).Msg("overlaps with local certs: skipped")
			continue
		}

		o.CertificateData = append(o.CertificateData, c)
	}
}

// ApplySettings modifies the config options using the given protobuf settings.
func (o *Options) ApplySettings(ctx context.Context, certsIndex *cryptutil.CertificatesIndex, settings *config.Settings) {
	if settings == nil {
		return
	}

	set(&o.InstallationID, settings.InstallationId)
	setLogLevel(&o.LogLevel, settings.LogLevel)
	setAccessLogFields(&o.AccessLogFields, settings.AccessLogFields)
	setAuthorizeLogFields(&o.AuthorizeLogFields, settings.AuthorizeLogFields)
	setLogLevel(&o.ProxyLogLevel, settings.ProxyLogLevel)
	set(&o.SharedKey, settings.SharedSecret)
	set(&o.Services, settings.Services)
	set(&o.Addr, settings.Address)
	set(&o.InsecureServer, settings.InsecureServer)
	set(&o.DNSLookupFamily, settings.DnsLookupFamily)
	o.applyExternalCerts(ctx, certsIndex, settings.GetCertificates())
	set(&o.HTTPRedirectAddr, settings.HttpRedirectAddr)
	setDuration(&o.ReadTimeout, settings.TimeoutRead)
	setDuration(&o.WriteTimeout, settings.TimeoutWrite)
	setDuration(&o.IdleTimeout, settings.TimeoutIdle)
	set(&o.AuthenticateURLString, settings.AuthenticateServiceUrl)
	set(&o.AuthenticateInternalURLString, settings.AuthenticateInternalServiceUrl)
	set(&o.SignOutRedirectURLString, settings.SignoutRedirectUrl)
	set(&o.AuthenticateCallbackPath, settings.AuthenticateCallbackPath)
	set(&o.CookieName, settings.CookieName)
	set(&o.CookieSecret, settings.CookieSecret)
	set(&o.CookieDomain, settings.CookieDomain)
	set(&o.CookieSecure, settings.CookieSecure)
	set(&o.CookieHTTPOnly, settings.CookieHttpOnly)
	setDuration(&o.CookieExpire, settings.CookieExpire)
	set(&o.CookieSameSite, settings.CookieSameSite)
	set(&o.ClientID, settings.IdpClientId)
	set(&o.ClientSecret, settings.IdpClientSecret)
	set(&o.Provider, settings.IdpProvider)
	set(&o.ProviderURL, settings.IdpProviderUrl)
	setSlice(&o.Scopes, settings.Scopes)
	setMap(&o.RequestParams, settings.RequestParams)
	setSlice(&o.AuthorizeURLStrings, settings.AuthorizeServiceUrls)
	set(&o.AuthorizeInternalURLString, settings.AuthorizeInternalServiceUrl)
	set(&o.OverrideCertificateName, settings.OverrideCertificateName)
	set(&o.CA, settings.CertificateAuthority)
	setOptional(&o.DeriveInternalDomainCert, settings.DeriveTls)
	set(&o.SigningKey, settings.SigningKey)
	setMap(&o.SetResponseHeaders, settings.SetResponseHeaders)
	setMap(&o.JWTClaimsHeaders, settings.JwtClaimsHeaders)
	setDuration(&o.DefaultUpstreamTimeout, settings.DefaultUpstreamTimeout)
	set(&o.MetricsAddr, settings.MetricsAddress)
	set(&o.MetricsBasicAuth, settings.MetricsBasicAuth)
	setCertificate(&o.MetricsCertificate, &o.MetricsCertificateKey, settings.MetricsCertificate)
	set(&o.MetricsClientCA, settings.MetricsClientCa)
	set(&o.TracingProvider, settings.TracingProvider)
	set(&o.TracingSampleRate, settings.TracingSampleRate)
	set(&o.TracingDatadogAddress, settings.TracingDatadogAddress)
	set(&o.TracingJaegerCollectorEndpoint, settings.TracingJaegerCollectorEndpoint)
	set(&o.TracingJaegerAgentEndpoint, settings.TracingJaegerAgentEndpoint)
	set(&o.ZipkinEndpoint, settings.TracingZipkinEndpoint)
	set(&o.GRPCAddr, settings.GrpcAddress)
	setOptional(&o.GRPCInsecure, settings.GrpcInsecure)
	setDuration(&o.GRPCClientTimeout, settings.GrpcClientTimeout)
	set(&o.GRPCClientDNSRoundRobin, settings.GrpcClientDnsRoundrobin)
	setSlice(&o.DataBrokerURLStrings, settings.DatabrokerServiceUrls)
	set(&o.DataBrokerInternalURLString, settings.DatabrokerInternalServiceUrl)
	set(&o.DataBrokerStorageType, settings.DatabrokerStorageType)
	set(&o.DataBrokerStorageConnectionString, settings.DatabrokerStorageConnectionString)
	set(&o.DataBrokerStorageCertSkipVerify, settings.DatabrokerStorageTlsSkipVerify)
	o.DownstreamMTLS.applySettingsProto(ctx, settings.DownstreamMtls)
	set(&o.GoogleCloudServerlessAuthenticationServiceAccount, settings.GoogleCloudServerlessAuthenticationServiceAccount)
	set(&o.UseProxyProtocol, settings.UseProxyProtocol)
	set(&o.AutocertOptions.Enable, settings.Autocert)
	set(&o.AutocertOptions.CA, settings.AutocertCa)
	set(&o.AutocertOptions.Email, settings.AutocertEmail)
	set(&o.AutocertOptions.EABKeyID, settings.AutocertEabKeyId)
	set(&o.AutocertOptions.EABMACKey, settings.AutocertEabMacKey)
	set(&o.AutocertOptions.UseStaging, settings.AutocertUseStaging)
	set(&o.AutocertOptions.MustStaple, settings.AutocertMustStaple)
	set(&o.AutocertOptions.Folder, settings.AutocertDir)
	set(&o.AutocertOptions.TrustedCA, settings.AutocertTrustedCa)
	set(&o.SkipXffAppend, settings.SkipXffAppend)
	set(&o.XffNumTrustedHops, settings.XffNumTrustedHops)
	setSlice(&o.ProgrammaticRedirectDomainWhitelist, settings.ProgrammaticRedirectDomainWhitelist)
	setAuditKey(&o.AuditKey, settings.AuditKey)
	setCodecType(&o.CodecType, settings.CodecType)
	setOptional(&o.PassIdentityHeaders, settings.PassIdentityHeaders)
	o.BrandingOptions = settings
}

func dataDir() string {
	homeDir, _ := os.UserHomeDir()
	if homeDir == "" {
		homeDir = "."
	}
	baseDir := filepath.Join(homeDir, ".local", "share")
	if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
		baseDir = xdgData
	}
	return filepath.Join(baseDir, "pomerium")
}

func compareByteSliceSlice(a, b [][]byte) int {
	sz := min(len(a), len(b))
	for i := 0; i < sz; i++ {
		switch bytes.Compare(a[i], b[i]) {
		case -1:
			return -1
		case 1:
			return 1
		}
	}

	switch {
	case len(a) < len(b):
		return -1
	case len(b) < len(a):
		return 1
	default:
		return 0
	}
}

func min(x, y int) int {
	if x < y {
		return x
	}
	return y
}

// NewAtomicOptions creates a new AtomicOptions.
func NewAtomicOptions() *atomicutil.Value[*Options] {
	return atomicutil.NewValue(new(Options))
}

func set[T any](dst, src *T) {
	if src == nil {
		return
	}
	*dst = *src
}

func setAccessLogFields(dst *[]log.AccessLogField, src *config.Settings_StringList) {
	if src == nil {
		return
	}
	*dst = make([]log.AccessLogField, len(src.Values))
	for i, v := range src.Values {
		(*dst)[i] = log.AccessLogField(v)
	}
}

func setAuthorizeLogFields(dst *[]log.AuthorizeLogField, src *config.Settings_StringList) {
	if src == nil {
		return
	}
	*dst = make([]log.AuthorizeLogField, len(src.Values))
	for i, v := range src.Values {
		(*dst)[i] = log.AuthorizeLogField(v)
	}
}

func setAuditKey(dst **PublicKeyEncryptionKeyOptions, src *crypt.PublicKeyEncryptionKey) {
	if src == nil {
		return
	}
	*dst = &PublicKeyEncryptionKeyOptions{
		ID:   src.GetId(),
		Data: base64.StdEncoding.EncodeToString(src.GetData()),
	}
}

func setCodecType(dst *CodecType, src *envoy_http_connection_manager.HttpConnectionManager_CodecType) {
	if src == nil {
		return
	}
	*dst = CodecTypeFromEnvoy(*src)
}

func setDuration(dst *time.Duration, src *durationpb.Duration) {
	if src == nil {
		return
	}
	*dst = src.AsDuration()
}

func setLogLevel(dst *LogLevel, src *string) {
	if src == nil {
		return
	}
	*dst = LogLevel(*src)
}

func setOptional[T any](dst **T, src *T) {
	if src == nil {
		return
	}
	v := *src
	*dst = &v
}

func setSlice[T any](dst *[]T, src []T) {
	if len(src) == 0 {
		return
	}
	*dst = src
}

func setMap[TKey comparable, TValue any, TMap ~map[TKey]TValue](dst *TMap, src map[TKey]TValue) {
	if len(src) == 0 {
		return
	}
	*dst = src
}

func setCertificate(
	dstCertificate *string,
	dstCertificateKey *string,
	src *config.Settings_Certificate,
) {
	if src == nil {
		return
	}
	if len(src.GetCertBytes()) > 0 {
		*dstCertificate = base64.StdEncoding.EncodeToString(src.GetCertBytes())
	}
	if len(src.GetKeyBytes()) > 0 {
		*dstCertificateKey = base64.StdEncoding.EncodeToString(src.GetKeyBytes())
	}
}