package server

import (
	"context"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"time"

	basicAuth "github.com/abbot/go-http-auth"
	"github.com/influxdata/chronograf"
	idgen "github.com/influxdata/chronograf/id"
	"github.com/influxdata/chronograf/influx"
	"github.com/influxdata/chronograf/kv"
	"github.com/influxdata/chronograf/kv/bolt"
	"github.com/influxdata/chronograf/kv/etcd"
	clog "github.com/influxdata/chronograf/log"
	"github.com/influxdata/chronograf/oauth2"
	"github.com/influxdata/chronograf/server/config"
	client "github.com/influxdata/usage-client/v1"
	flags "github.com/jessevdk/go-flags"
)

var (
	startTime time.Time
	errNoAuth = errors.New("no auth configured")
)

func init() {
	startTime = time.Now().UTC()
}

// Server for the chronograf API
type Server struct {
	Host        string `long:"host" description:"The IP to listen on" default:"0.0.0.0" env:"HOST"`
	Port        int    `long:"port" description:"The port to listen on for insecure connections, defaults to a random value" default:"8888" env:"PORT"`
	DisableGZip bool   `long:"disable-gzip" description:"Disables gzip compression, even if client requests it. Useful if running on a low-cpu device" env:"DISABLE_GZIP"`

	PprofEnabled bool `long:"pprof-enabled" description:"Enable the /debug/pprof/* HTTP routes" env:"PPROF_ENABLED"`

	Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"`
	Key  flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"`

	InfluxDBURL      string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"`
	InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"`
	InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"`
	InfluxDBOrg      string `long:"influxdb-org" description:"Organization for your InfluxDB v2 instance" env:"INFLUXDB_ORG"`
	InfluxDBToken    string `long:"influxdb-token" description:"Token for your InfluxDB v2 instance" env:"INFLUXDB_TOKEN"`

	KapacitorURL      string `long:"kapacitor-url" description:"Location of your Kapacitor instance" env:"KAPACITOR_URL"`
	KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"`
	KapacitorPassword string `long:"kapacitor-password" description:"Password of your Kapacitor instance" env:"KAPACITOR_PASSWORD"`

	Develop            bool          `short:"d" long:"develop" description:"Run server in develop mode."`
	BoltPath           string        `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
	CannedPath         string        `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"`
	ProtoboardsPath    string        `long:"protoboards-path" description:"Path to directory of protoboards (/usr/share/chronograf/protoboards)" env:"PROTOBOARDS_PATH" default:"protoboards"`
	ResourcesPath      string        `long:"resources-path" description:"Path to directory of pre-canned dashboards, sources, kapacitors, and organizations (/usr/share/chronograf/resources)" env:"RESOURCES_PATH" default:"canned"`
	TokenSecret        string        `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"`
	JwksURL            string        `long:"jwks-url" description:"URL that returns OpenID Key Discovery JWKS document." env:"JWKS_URL"`
	UseIDToken         bool          `long:"use-id-token" description:"Enable id_token processing." env:"USE_ID_TOKEN"`
	LoginHint          string        `long:"login-hint" description:"OpenID login_hint paramter to passed to authorization server during authentication" env:"LOGIN_HINT"`
	AuthDuration       time.Duration `long:"auth-duration" default:"720h" description:"Total duration of cookie life for authentication (in hours). 0 means authentication expires on browser close." env:"AUTH_DURATION"`
	InactivityDuration time.Duration `long:"inactivity-duration" default:"5m" description:"Duration for which a token is valid without any new activity." env:"INACTIVITY_DURATION"`

	GithubClientID     string   `short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID"`
	GithubClientSecret string   `short:"s" long:"github-client-secret" description:"Github Client Secret for OAuth 2 support" env:"GH_CLIENT_SECRET"`
	GithubOrgs         []string `short:"o" long:"github-organization" description:"Github organization user is required to have active membership" env:"GH_ORGS" env-delim:","`
	GithubURL          string   `long:"github-url" description:"Github base URL must be specified for Github Enterprise." default:"https://github.com" env:"GH_URL"`

	EtcdEndpoints      []string       `short:"e" long:"etcd-endpoints" description:"List of etcd endpoints" env:"ETCD_ENDPOINTS" env-delim:","`
	EtcdUsername       string         `long:"etcd-username" description:"Username to log into etcd." env:"ETCD_USERNAME"`
	EtcdPassword       string         `long:"etcd-password" description:"Password to log into etcd." env:"ETCD_PASSWORD"`
	EtcdDialTimeout    time.Duration  `long:"etcd-dial-timeout" default:"-1s" description:"Total time to wait before timing out while connecting to etcd endpoints. 0 means no timeout. " env:"ETCD_DIAL_TIMEOUT"`
	EtcdRequestTimeout time.Duration  `long:"etcd-request-timeout" default:"-1s" description:"Total time to wait before timing out the etcd view or update. 0 means no timeout." env:"ETCD_REQUEST_TIMEOUT"`
	EtcdCert           flags.Filename `long:"etcd-cert" description:"Path to PEM encoded TLS public key certificate. " env:"ETCD_CERTIFICATE"`
	EtcdKey            flags.Filename `long:"etcd-key" description:"Path to private key associated with given certificate. " env:"ETCD_PRIVATE_KEY"`
	EtcdRootCA         flags.Filename `long:"etcd-root-ca" description:"File location of root CA cert for TLS verification." env:"ETCD_ROOT_CA"`

	GoogleClientID     string   `long:"google-client-id" description:"Google Client ID for OAuth 2 support" env:"GOOGLE_CLIENT_ID"`
	GoogleClientSecret string   `long:"google-client-secret" description:"Google Client Secret for OAuth 2 support" env:"GOOGLE_CLIENT_SECRET"`
	GoogleDomains      []string `long:"google-domains" description:"Google email domain user is required to have active membership" env:"GOOGLE_DOMAINS" env-delim:","`
	PublicURL          string   `long:"public-url" description:"Full public URL used to access Chronograf from a web browser. Used for OAuth2 authentication. (http://localhost:8888)" env:"PUBLIC_URL"`

	HerokuClientID      string   `long:"heroku-client-id" description:"Heroku Client ID for OAuth 2 support" env:"HEROKU_CLIENT_ID"`
	HerokuSecret        string   `long:"heroku-secret" description:"Heroku Secret for OAuth 2 support" env:"HEROKU_SECRET"`
	HerokuOrganizations []string `long:"heroku-organization" description:"Heroku Organization Memberships a user is required to have for access to Chronograf (comma separated)" env:"HEROKU_ORGS" env-delim:","`

	GenericName         string         `long:"generic-name" description:"Generic OAuth2 name presented on the login page"  env:"GENERIC_NAME"`
	GenericClientID     string         `long:"generic-client-id" description:"Generic OAuth2 Client ID. Can be used own OAuth2 service."  env:"GENERIC_CLIENT_ID"`
	GenericClientSecret string         `long:"generic-client-secret" description:"Generic OAuth2 Client Secret" env:"GENERIC_CLIENT_SECRET"`
	GenericScopes       []string       `long:"generic-scopes" description:"Scopes requested by provider of web client." default:"user:email" env:"GENERIC_SCOPES" env-delim:","`
	GenericDomains      []string       `long:"generic-domains" description:"Email domain users' email address to have (example.com)" env:"GENERIC_DOMAINS" env-delim:","`
	GenericAuthURL      string         `long:"generic-auth-url" description:"OAuth 2.0 provider's authorization endpoint URL" env:"GENERIC_AUTH_URL"`
	GenericTokenURL     string         `long:"generic-token-url" description:"OAuth 2.0 provider's token endpoint URL" env:"GENERIC_TOKEN_URL"`
	GenericAPIURL       string         `long:"generic-api-url" description:"URL that returns OpenID UserInfo compatible information." env:"GENERIC_API_URL"`
	GenericAPIKey       string         `long:"generic-api-key" description:"JSON lookup key into OpenID UserInfo. (Azure should be userPrincipalName)" default:"email" env:"GENERIC_API_KEY"`
	GenericInsecure     bool           `long:"generic-insecure" description:"Whether or not to verify auth-url's tls certificates." env:"GENERIC_INSECURE"`
	GenericRootCA       flags.Filename `long:"generic-root-ca" description:"File location of root ca cert for generic oauth tls verification." env:"GENERIC_ROOT_CA"`
	OAuthNoPKCE         bool           `long:"oauth-no-pkce" description:"Disables OAuth PKCE." env:"OAUTH_NO_PKCE"`

	Auth0Domain        string   `long:"auth0-domain" description:"Subdomain of auth0.com used for Auth0 OAuth2 authentication" env:"AUTH0_DOMAIN"`
	Auth0ClientID      string   `long:"auth0-client-id" description:"Auth0 Client ID for OAuth2 support" env:"AUTH0_CLIENT_ID"`
	Auth0ClientSecret  string   `long:"auth0-client-secret" description:"Auth0 Client Secret for OAuth2 support" env:"AUTH0_CLIENT_SECRET"`
	Auth0Organizations []string `long:"auth0-organizations" description:"Auth0 organizations permitted to access Chronograf (comma separated)" env:"AUTH0_ORGS" env-delim:","`
	Auth0SuperAdminOrg string   `long:"auth0-superadmin-org" description:"Auth0 organization from which users are automatically granted SuperAdmin status" env:"AUTH0_SUPERADMIN_ORG"`

	RedirAuth string `long:"redir-auth-login" description:"Automatically redirect login to specified OAuth provider." env:"REDIR_AUTH_LOGIN"`

	PubKey          string         `long:"pub-key" description:"Public key or superadmin token authentication" env:"PUB_KEY"`
	PubKeyFile      flags.Filename `long:"pub-key-file" description:"File location of public key for superadmin token authentication." env:"PUB_KEY_FILE"`
	NonceExpiration time.Duration  `long:"nonce-expiration" default:"10m" description:"Duration in which a signed nonce is valid. Used for superadmin token authentication." env:"NONCE_EXPIRATION"`

	StatusFeedURL          string            `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://influxdata.com/feed/json" env:"STATUS_FEED_URL"`
	CustomLinks            map[string]string `long:"custom-link" description:"Custom link to be added to the client User menu. Multiple links can be added by using multiple of the same flag with different 'name:url' values, or as an environment variable with comma-separated 'name:url' values. E.g. via flags: '--custom-link=InfluxData:https://www.influxdata.com --custom-link=Chronograf:https://github.com/influxdata/chronograf'. E.g. via environment variable: 'export CUSTOM_LINKS=InfluxData:https://www.influxdata.com,Chronograf:https://github.com/influxdata/chronograf'" env:"CUSTOM_LINKS" env-delim:","`
	TelegrafSystemInterval time.Duration     `long:"telegraf-system-interval" default:"1m" description:"Duration used in the GROUP BY time interval for the hosts list" env:"TELEGRAF_SYSTEM_INTERVAL"`

	HostPageDisabled  bool   `short:"H" long:"host-page-disabled" description:"Disable the host list page" env:"HOST_PAGE_DISABLED"`
	ReportingDisabled bool   `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"`
	CustomAutoRefresh string `long:"custom-auto-refresh" description:"Adds custom auto refresh options using semicolon separated list of label=milliseconds pairs" env:"CUSTOM_AUTO_REFRESH"`
	LogLevel          string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
	Basepath          string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted. (Note: PREFIX_ROUTES has been deprecated. Now, if basepath is set, all routes will be prefixed with it.)" env:"BASE_PATH"`
	ShowVersion       bool   `short:"v" long:"version" description:"Show Chronograf version info"`
	BuildInfo         chronograf.BuildInfo

	BasicAuthRealm    string         `long:"basic-auth-realm" default:"Chronograf" description:"User visible basic authentication realm" env:"BASICAUTH_REALM"`
	BasicAuthHtpasswd flags.Filename `long:"htpasswd" description:"File location of .htpasswd file, turns on HTTP basic authentication when specified." env:"HTPASSWD"`

	TLSCiphers    string `long:"tls-ciphers" description:"Comma-separated list of cipher suites to use. Use 'help' cipher to print available ciphers." env:"TLS_CIPHERS"`
	TLSMinVersion string `long:"tls-min-version" description:"Minimum version of the TLS protocol that will be negotiated." default:"1.2" env:"TLS_MIN_VERSION"`
	TLSMaxVersion string `long:"tls-max-version" description:"Maximum version of the TLS protocol that will be negotiated." env:"TLS_MAX_VERSION"`

	oauthClient http.Client
}

func provide(p oauth2.Provider, m oauth2.Mux, ok func() error) func(func(oauth2.Provider, oauth2.Mux)) {
	return func(configure func(oauth2.Provider, oauth2.Mux)) {
		if err := ok(); err == nil {
			configure(p, m)
		}
	}
}

// UseGithub validates the CLI parameters to enable github oauth support
func (s *Server) UseGithub() error {
	errMsg := []string{}

	if s.TokenSecret != "" && s.GithubClientID != "" && s.GithubClientSecret != "" {
		return nil
	} else if s.GithubClientID == "" && s.GithubClientSecret == "" {
		return errNoAuth
	}

	if s.TokenSecret == "" {
		errMsg = append(errMsg, "token secret")
	}
	if s.GithubClientID == "" {
		errMsg = append(errMsg, "client id")
	}
	if s.GithubClientSecret == "" {
		errMsg = append(errMsg, "client secret")
	}
	if errMsg != nil {
		return fmt.Errorf("missing Github oauth setting[s]: %s", strings.Join(errMsg, ", "))
	}

	return nil
}

// UseGoogle validates the CLI parameters to enable google oauth support
func (s *Server) UseGoogle() error {
	errMsg := []string{}

	if s.TokenSecret != "" && s.GoogleClientID != "" && s.GoogleClientSecret != "" && s.PublicURL != "" {
		return nil
	} else if s.GoogleClientID == "" && s.GoogleClientSecret == "" {
		return errNoAuth
	}

	if s.TokenSecret == "" {
		errMsg = append(errMsg, "token secret")
	}
	if s.GoogleClientID == "" {
		errMsg = append(errMsg, "client id")
	}
	if s.GoogleClientSecret == "" {
		errMsg = append(errMsg, "client secret")
	}
	if s.PublicURL == "" {
		errMsg = append(errMsg, "public url")
	}
	if errMsg != nil {
		return fmt.Errorf("missing Google oauth setting[s]: %s", strings.Join(errMsg, ", "))
	}

	return nil
}

// UseHeroku validates the CLI parameters to enable heroku oauth support
func (s *Server) UseHeroku() error {
	errMsg := []string{}

	if s.TokenSecret != "" && s.HerokuClientID != "" && s.HerokuSecret != "" {
		return nil
	} else if s.HerokuClientID == "" && s.HerokuSecret == "" {
		return errNoAuth
	}

	if s.TokenSecret == "" {
		errMsg = append(errMsg, "token secret")
	}
	if s.HerokuClientID == "" {
		errMsg = append(errMsg, "client id")
	}
	if s.HerokuSecret == "" {
		errMsg = append(errMsg, "client secret")
	}
	if errMsg != nil {
		return fmt.Errorf("missing Heroku oauth setting[s]: %s", strings.Join(errMsg, ", "))
	}

	return nil
}

// UseAuth0 validates the CLI parameters to enable Auth0 oauth support
func (s *Server) UseAuth0() error {
	errMsg := []string{}

	if s.Auth0ClientID != "" && s.Auth0ClientSecret != "" {
		return nil
	} else if s.Auth0ClientID == "" && s.Auth0ClientSecret == "" {
		return errNoAuth
	}

	if s.Auth0ClientID == "" {
		errMsg = append(errMsg, "client id")
	}
	if s.Auth0ClientSecret == "" {
		errMsg = append(errMsg, "client secret")
	}
	if errMsg != nil {
		return fmt.Errorf("missing Auth0 oauth setting[s]: %s", strings.Join(errMsg, ", "))
	}

	return nil
}

// UseGenericOAuth2 validates the CLI parameters to enable generic oauth support
func (s *Server) UseGenericOAuth2() error {
	errMsg := []string{}

	if s.TokenSecret != "" && s.GenericClientID != "" &&
		s.GenericClientSecret != "" && s.GenericAuthURL != "" &&
		s.GenericTokenURL != "" {
		return nil
	} else if s.GenericClientID == "" && s.GenericClientSecret == "" &&
		s.GenericAuthURL == "" && s.GenericTokenURL == "" {
		return errNoAuth
	}

	if s.TokenSecret == "" {
		errMsg = append(errMsg, "token secret")
	}
	if s.GenericClientID == "" {
		errMsg = append(errMsg, "client id")
	}
	if s.GenericClientSecret == "" {
		errMsg = append(errMsg, "client secret")
	}
	if s.GenericAuthURL == "" {
		errMsg = append(errMsg, "auth url")
	}
	if s.GenericTokenURL == "" {
		errMsg = append(errMsg, "token url")
	}
	if errMsg != nil {
		return fmt.Errorf("missing Generic oauth setting[s]: %s", strings.Join(errMsg, ", "))
	}

	return nil
}

// getCerts gets the read certs from rootPath to the systemCerts.
func getCerts(rootPath string) (*x509.CertPool, error) {
	if rootPath == "" {
		return nil, nil
	}

	f, err := os.Open(rootPath)
	if err != nil {
		return nil, err
	}

	defer f.Close()
	return processCerts(f)
}

func processCerts(rootReader io.Reader) (*x509.CertPool, error) {
	certPool, err := x509.SystemCertPool()
	if err != nil {
		return nil, fmt.Errorf("error using system cert pool: %s", err.Error())
	}

	certs, err := ioutil.ReadAll(rootReader)
	if err != nil {
		return nil, fmt.Errorf("error reading generic root ca: %s", err.Error())
	}

	ok := certPool.AppendCertsFromPEM(certs)
	if !ok {
		return nil, errors.New("error appending cert from root ca")
	}

	return certPool, nil
}
func (s *Server) createCodeExchange() oauth2.CodeExchange {
	return oauth2.NewCodeExchange(!s.OAuthNoPKCE, s.TokenSecret)
}

func (s *Server) githubOAuth(logger chronograf.Logger, auth oauth2.Authenticator) (oauth2.Provider, oauth2.Mux, func() error) {
	gh := oauth2.Github{
		ClientID:     s.GithubClientID,
		ClientSecret: s.GithubClientSecret,
		Orgs:         s.GithubOrgs,
		BaseURL:      s.GithubURL,
		Logger:       logger,
	}
	jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
	ghMux := oauth2.NewAuthMux(&gh, auth, jwt, s.Basepath, logger, s.UseIDToken, s.LoginHint, &s.oauthClient, s.createCodeExchange())
	return &gh, ghMux, s.UseGithub
}

func (s *Server) googleOAuth(logger chronograf.Logger, auth oauth2.Authenticator) (oauth2.Provider, oauth2.Mux, func() error) {
	redirectURL := s.PublicURL + s.Basepath + "/oauth/google/callback"
	google := oauth2.Google{
		ClientID:     s.GoogleClientID,
		ClientSecret: s.GoogleClientSecret,
		Domains:      s.GoogleDomains,
		RedirectURL:  redirectURL,
		Logger:       logger,
	}
	jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
	goMux := oauth2.NewAuthMux(&google, auth, jwt, s.Basepath, logger, s.UseIDToken, s.LoginHint, &s.oauthClient, s.createCodeExchange())
	return &google, goMux, s.UseGoogle
}

func (s *Server) herokuOAuth(logger chronograf.Logger, auth oauth2.Authenticator) (oauth2.Provider, oauth2.Mux, func() error) {
	heroku := oauth2.Heroku{
		ClientID:      s.HerokuClientID,
		ClientSecret:  s.HerokuSecret,
		Organizations: s.HerokuOrganizations,
		Logger:        logger,
	}
	jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
	hMux := oauth2.NewAuthMux(&heroku, auth, jwt, s.Basepath, logger, s.UseIDToken, s.LoginHint, &s.oauthClient, s.createCodeExchange())
	return &heroku, hMux, s.UseHeroku
}

func (s *Server) genericOAuth(logger chronograf.Logger, auth oauth2.Authenticator) (oauth2.Provider, oauth2.Mux, func() error) {
	gen := oauth2.Generic{
		PageName:       s.GenericName,
		ClientID:       s.GenericClientID,
		ClientSecret:   s.GenericClientSecret,
		RequiredScopes: s.GenericScopes,
		Domains:        s.GenericDomains,
		RedirectURL:    s.genericRedirectURL(),
		AuthURL:        s.GenericAuthURL,
		TokenURL:       s.GenericTokenURL,
		APIURL:         s.GenericAPIURL,
		APIKey:         s.GenericAPIKey,
		Logger:         logger,
	}
	jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
	genMux := oauth2.NewAuthMux(&gen, auth, jwt, s.Basepath, logger, s.UseIDToken, s.LoginHint, &s.oauthClient, s.createCodeExchange())
	return &gen, genMux, s.UseGenericOAuth2
}

func (s *Server) auth0OAuth(logger chronograf.Logger, auth oauth2.Authenticator) (oauth2.Provider, oauth2.Mux, func() error) {
	redirectPath := path.Join(s.Basepath, "oauth", "auth0", "callback")
	redirectURL, err := url.Parse(s.PublicURL)
	if err != nil {
		logger.Error("Error parsing public URL: err:", err)
		return &oauth2.Auth0{}, &oauth2.AuthMux{}, func() error { return fmt.Errorf("failed to parse public URL: %s", err.Error()) }
	}
	redirectURL.Path = redirectPath

	auth0, err := oauth2.NewAuth0(s.Auth0Domain, s.Auth0ClientID, s.Auth0ClientSecret, redirectURL.String(), s.Auth0Organizations, logger)

	jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
	genMux := oauth2.NewAuthMux(&auth0, auth, jwt, s.Basepath, logger, s.UseIDToken, s.LoginHint, &s.oauthClient, s.createCodeExchange())

	if err != nil {
		logger.Error("Error parsing Auth0 domain: err:", err)
		return &auth0, genMux, func() error { return fmt.Errorf("failed to parse Auth0 domain: %s", err.Error()) }
	}
	return &auth0, genMux, s.UseAuth0
}

func (s *Server) genericRedirectURL() string {
	if s.PublicURL == "" {
		return ""
	}

	genericName := "generic"
	if s.GenericName != "" {
		genericName = s.GenericName
	}

	publicURL, err := url.Parse(s.PublicURL)
	if err != nil {
		return ""
	}

	publicURL.Path = path.Join(publicURL.Path, s.Basepath, "oauth", genericName, "callback")
	return publicURL.String()
}

func (s *Server) useAuth() bool {
	useAuths := []func() error{
		s.UseGithub,
		s.UseGoogle,
		s.UseHeroku,
		s.UseGenericOAuth2,
		s.UseAuth0,
	}

	var err error
	for i := range useAuths {
		switch err = useAuths[i](); err {
		case nil:
			return true
		case errNoAuth:
			continue
		default:
			// If there was an attempt to configure authentication,
			// chronograf should not disable authentication.
			return true
		}
	}

	return false
}

func (s *Server) validateAuth() error {
	useAuths := []func() error{
		s.UseGithub,
		s.UseGoogle,
		s.UseHeroku,
		s.UseGenericOAuth2,
		s.UseAuth0,
	}

	var errs []string
	for i := range useAuths {
		if err := useAuths[i](); err != nil && err != errNoAuth {
			errs = append(errs, err.Error())
		}
	}

	if !s.useAuth() && s.TokenSecret != "" {
		errs = append(errs, "token secret without oauth config is invalid")
	}

	if len(errs) == 0 {
		return nil
	}

	return errors.New(strings.Join(errs, "; "))
}

func (s *Server) useTLS() bool {
	return s.Cert != ""
}

// NewListener will return an http or https listener depending useTLS().
func (s *Server) NewListener() (net.Listener, error) {
	addr := net.JoinHostPort(s.Host, strconv.Itoa(s.Port))
	if !s.useTLS() {
		listener, err := net.Listen("tcp", addr)
		if err != nil {
			return nil, err
		}
		return listener, nil
	}

	tlsConfig, err := config.CreateTLSConfig(config.TLSOptions{
		Cert:       string(s.Cert),
		Key:        string(s.Key),
		Ciphers:    strings.Split(s.TLSCiphers, ","),
		MinVersion: s.TLSMinVersion,
		MaxVersion: s.TLSMaxVersion,
	})
	if err != nil {
		return nil, err
	}

	listener, err := tls.Listen("tcp", addr, tlsConfig)
	if err != nil {
		return nil, err
	}

	return listener, nil
}

type builders struct {
	Layouts       LayoutBuilder
	Sources       SourcesBuilder
	Kapacitors    KapacitorBuilder
	Dashboards    DashboardBuilder
	Organizations OrganizationBuilder
	Protoboards   ProtoboardsBuilder
}

func (s *Server) newBuilders(logger chronograf.Logger) builders {
	return builders{
		Layouts: &MultiLayoutBuilder{
			Logger:     logger,
			UUID:       &idgen.UUID{},
			CannedPath: s.CannedPath,
		},
		Dashboards: &MultiDashboardBuilder{
			Logger: logger,
			ID:     idgen.NewTime(),
			Path:   s.ResourcesPath,
		},
		Sources: &MultiSourceBuilder{
			InfluxDBURL:      s.InfluxDBURL,
			InfluxDBUsername: s.InfluxDBUsername,
			InfluxDBPassword: s.InfluxDBPassword,
			InfluxDBOrg:      s.InfluxDBOrg,
			InfluxDBToken:    s.InfluxDBToken,
			Logger:           logger,
			ID:               idgen.NewTime(),
			Path:             s.ResourcesPath,
		},
		Kapacitors: &MultiKapacitorBuilder{
			KapacitorURL:      s.KapacitorURL,
			KapacitorUsername: s.KapacitorUsername,
			KapacitorPassword: s.KapacitorPassword,
			Logger:            logger,
			ID:                idgen.NewTime(),
			Path:              s.ResourcesPath,
		},
		Organizations: &MultiOrganizationBuilder{
			Logger: logger,
			Path:   s.ResourcesPath,
		},
		Protoboards: &MultiProtoboardsBuilder{
			Logger:          logger,
			UUID:            &idgen.UUID{},
			ProtoboardsPath: s.ProtoboardsPath,
		},
	}
}

var publicKey *rsa.PublicKey // pubKey is for the simple super admin jwt-esque check.

// Set the public key preferring from file, if set.
func (s *Server) setPubkey() error {
	pubKey := []byte(s.PubKey)

	if fil := s.PubKeyFile; fil != "" {
		key, err := ioutil.ReadFile(string(s.PubKeyFile))
		if err != nil {
			return err
		}
		pubKey = key
	}

	if len(pubKey) == 0 {
		return nil
	}

	block, _ := pem.Decode(pubKey)
	if block == nil {
		return errors.New("no key found")
	} else if block.Type != "PUBLIC KEY" {
		return fmt.Errorf("unsupported key type %q", block.Type)
	}

	var err error
	publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
	return err
}

// Serve starts and runs the chronograf server
func (s *Server) Serve(ctx context.Context) {
	go rotateSuperAdminNonce(ctx, s.NonceExpiration)

	logger := clog.New(clog.ParseLevel(s.LogLevel))
	customLinks, err := NewCustomLinks(s.CustomLinks)
	if err != nil {
		logger.
			WithField("component", "server").
			WithField("CustomLink", "invalid").
			Error(err)
		return
	}

	err = s.setPubkey()
	if err != nil {
		logger.Error("Unable to set public key ", err)
		os.Exit(1)
	}

	var db kv.Store
	if len(s.EtcdEndpoints) == 0 {
		db, err = bolt.NewClient(ctx,
			bolt.WithPath(s.BoltPath),
			bolt.WithLogger(logger),
			bolt.WithBuildInfo(s.BuildInfo),
		)
		if err != nil {
			logger.Error("Unable to create bolt client", err)
			os.Exit(1)
		}

	} else {
		var tlsConfig *tls.Config
		if s.EtcdCert != "" {
			tlsConfig, err = config.CreateTLSConfig(config.TLSOptions{
				Cert:    string(s.EtcdCert),
				Key:     string(s.EtcdKey),
				CACerts: string(s.EtcdRootCA),
			})
			if err != nil {
				logger.Error("Unable to create TLS configuration for etcd client", err)
				os.Exit(1)
			}
		}

		db, err = etcd.NewClient(ctx,
			etcd.WithEndpoints(s.EtcdEndpoints),
			etcd.WithLogin(s.EtcdUsername, s.EtcdPassword),
			etcd.WithRequestTimeout(s.EtcdRequestTimeout),
			etcd.WithDialTimeout(s.EtcdDialTimeout),
			etcd.WithLogger(logger),
			etcd.WithTLS(tlsConfig),
		)
		if err != nil {
			logger.Error("Unable to create etcd client", err)
			os.Exit(1)
		}
	}

	service := openService(ctx, db, s.newBuilders(logger), logger, s.useAuth())
	service.SuperAdminProviderGroups = superAdminProviderGroups{
		auth0: s.Auth0SuperAdminOrg,
	}
	service.Env = chronograf.Environment{
		TelegrafSystemInterval: s.TelegrafSystemInterval,
		HostPageDisabled:       s.HostPageDisabled,
		CustomAutoRefresh:      s.CustomAutoRefresh,
	}

	if !validBasepath(s.Basepath) {
		err := fmt.Errorf("invalid basepath, must follow format \"/mybasepath\"")
		logger.
			WithField("component", "server").
			WithField("basepath", "invalid").
			Error(err)
		return
	}

	if err = s.validateAuth(); err != nil {
		logger.
			WithField("component", "server").
			WithField("basepath", "invalid").
			Error(fmt.Errorf("failed to validate Oauth settings: %s", err))
		return
	}

	certs, err := getCerts(string(s.GenericRootCA))
	if err != nil {
		logger.Error(err)
		return
	}

	s.oauthClient = http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: s.GenericInsecure,
				RootCAs:            certs,
			},
		},
	}

	auth := oauth2.NewCookieJWT(s.TokenSecret, s.AuthDuration, s.InactivityDuration)
	providerFuncs := []func(func(oauth2.Provider, oauth2.Mux)){
		provide(s.githubOAuth(logger, auth)),
		provide(s.googleOAuth(logger, auth)),
		provide(s.herokuOAuth(logger, auth)),
		provide(s.genericOAuth(logger, auth)),
		provide(s.auth0OAuth(logger, auth)),
	}

	var basicAuthenticator *basicAuth.BasicAuth
	if !s.useAuth() && len(s.BasicAuthHtpasswd) > 0 {
		logger.
			WithField("component", "server").
			WithField("realm", s.BasicAuthRealm).
			WithField("htpasswd", s.BasicAuthHtpasswd).
			Info("Configuring HTTP basic authentication")
		basicAuthenticator = basicAuth.NewBasicAuthenticator(
			s.BasicAuthRealm,
			basicAuth.HtpasswdFileProvider(string(s.BasicAuthHtpasswd)),
		)
	}

	handler := NewMux(MuxOpts{
		Develop:       s.Develop,
		Auth:          auth,
		Logger:        logger,
		UseAuth:       s.useAuth(),
		RedirAuth:     s.RedirAuth,
		ProviderFuncs: providerFuncs,
		Basepath:      s.Basepath,
		StatusFeedURL: s.StatusFeedURL,
		CustomLinks:   customLinks,
		PprofEnabled:  s.PprofEnabled,
		DisableGZip:   s.DisableGZip,
		nonceExpire:   s.NonceExpiration,
		BasicAuth:     basicAuthenticator,
	}, service)

	// Add chronograf's version header to all requests
	handler = version(s.BuildInfo.Version, handler)

	if s.useTLS() {
		// Add HSTS to instruct all browsers to change from http to https
		handler = hsts(handler)
	}

	// Using a log writer for http server logging
	w := logger.Writer()
	defer w.Close()
	stdLog := log.New(w, "", 0)

	httpServer := &http.Server{
		ErrorLog:    stdLog,
		Handler:     handler,
		IdleTimeout: 5 * time.Second,
	}

	httpServer.SetKeepAlivesEnabled(true)

	if !s.ReportingDisabled {
		go reportUsageStats(s.BuildInfo, logger)
	}
	scheme := "http"
	if s.useTLS() {
		scheme = "https"
	}

	listener, err := s.NewListener()
	if err != nil {
		logger.
			WithField("component", "server").
			Error(err)
		return
	}
	defer listener.Close()

	logger.
		WithField("component", "server").
		Info("Serving chronograf at ", scheme, "://", listener.Addr())

	if err := httpServer.Serve(listener); err != nil {
		logger.
			WithField("component", "server").
			Error(err)
		return
	}

	logger.
		WithField("component", "server").
		Info("Stopped serving chronograf at ", scheme, "://", listener.Addr())
}

func openService(ctx context.Context, db kv.Store, builder builders, logger chronograf.Logger, useAuth bool) Service {
	svc, err := kv.NewService(ctx, db, kv.WithLogger(logger))
	if err != nil {
		logger.Error("Unable to create kv service", err)
		os.Exit(1)
	}

	dashboards, err := builder.Dashboards.Build(svc.DashboardsStore())
	if err != nil {
		logger.
			WithField("component", "DashboardsStore").
			Error("Unable to construct a MultiDashboardsStore", err)
		os.Exit(1)
	}

	organizations, err := builder.Organizations.Build(svc.OrganizationsStore())
	if err != nil {
		logger.
			WithField("component", "OrganizationsStore").
			Error("Unable to construct a MultiOrganizationStore", err)
		os.Exit(1)
	}

	kapacitors, err := builder.Kapacitors.Build(svc.ServersStore())
	if err != nil {
		logger.
			WithField("component", "KapacitorStore").
			Error("Unable to construct a MultiKapacitorStore", err)
		os.Exit(1)
	}

	sources, err := builder.Sources.Build(svc.SourcesStore())
	if err != nil {
		logger.
			WithField("component", "SourcesStore").
			Error("Unable to construct a MultiSourcesStore", err)
		os.Exit(1)
	}

	protoboards, err := builder.Protoboards.Build()
	if err != nil {
		logger.
			WithField("component", "Protoboards").
			Error("Unable to construct a MultiLayoutsStore", err)
		os.Exit(1)
	}

	layouts, err := builder.Layouts.Build()
	if err != nil {
		logger.
			WithField("component", "LayoutsStore").
			Error("Unable to construct a MultiLayoutsStore", err)
		os.Exit(1)
	}

	return Service{
		TimeSeriesClient: &InfluxClient{},
		Store: &Store{
			LayoutsStore:            layouts,
			DashboardsStore:         dashboards,
			SourcesStore:            sources,
			ServersStore:            kapacitors,
			OrganizationsStore:      organizations,
			ProtoboardsStore:        protoboards,
			UsersStore:              svc.UsersStore(),
			ConfigStore:             svc.ConfigStore(),
			MappingsStore:           svc.MappingsStore(),
			OrganizationConfigStore: svc.OrganizationConfigStore(),
		},
		Logger:    logger,
		UseAuth:   useAuth,
		Databases: &influx.Client{Logger: logger},
	}
}

// reportUsageStats starts periodic server reporting.
func reportUsageStats(bi chronograf.BuildInfo, logger chronograf.Logger) {
	rand.Seed(time.Now().UTC().UnixNano())
	serverID := strconv.FormatUint(uint64(rand.Int63()), 10)
	reporter := client.New("")
	values := client.Values{
		"os":         runtime.GOOS,
		"arch":       runtime.GOARCH,
		"version":    bi.Version,
		"cluster_id": serverID,
		"uptime":     time.Since(startTime).Seconds(),
	}
	l := logger.WithField("component", "usage").
		WithField("reporting_addr", reporter.URL).
		WithField("freq", "24h").
		WithField("stats", "os,arch,version,cluster_id,uptime")
	l.Info("Reporting usage stats")
	reporter.Save(clientUsage(values))

	ticker := time.NewTicker(24 * time.Hour)
	defer ticker.Stop()
	for {
		<-ticker.C
		values["uptime"] = time.Since(startTime).Seconds()
		l.Debug("Reporting usage stats")
		go reporter.Save(clientUsage(values))
	}
}

func clientUsage(values client.Values) *client.Usage {
	return &client.Usage{
		Product: "chronograf-ng",
		Data: []client.UsageData{
			{
				Values: values,
			},
		},
	}
}

var re = regexp.MustCompile(`(\/{1}[\w-]+)+`)

func validBasepath(basepath string) bool {
	return re.ReplaceAllLiteralString(basepath, "") == ""
}
