view pkg/config/config.go @ 5711:2dd155cc95ec revive-cleanup

Fix all revive issue (w/o machine generated stuff).
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Tue, 20 Feb 2024 22:22:57 +0100
parents a826d84485c8
children
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2018, 2019 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
//  * Bernhard E. Reiter <bernhard.reiter@intevation.de>

// Package config holds the configuration of the gemma server.
package config

import (
	"crypto/sha256"
	"fmt"
	lg "log"
	"sync"
	"time"

	homedir "github.com/mitchellh/go-homedir"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"

	"gemma.intevation.de/gemma/pkg/common"
	"gemma.intevation.de/gemma/pkg/log"
)

// This is not part of the persistent config.
var configFile string

// DBHost is the hostname of the database.
func DBHost() string { return viper.GetString("db-host") }

// DBPort is the port of the database.
func DBPort() uint { return uint(viper.GetInt32("db-port")) }

// DBName is the name of the database.
func DBName() string { return viper.GetString("db-name") }

// DBUser is the name of the user to connect to the database.
func DBUser() string { return viper.GetString("db-user") }

// DBPassword is the password to connect to the database.
func DBPassword() string { return viper.GetString("db-password") }

// DBSSLMode selects the SSL mode to encrypt the connection to the database.
func DBSSLMode() string { return viper.GetString("db-ssl") }

// SessionStore is the path to the session store.
// If empty the session store is kept in memory only.
func SessionStore() string { return viper.GetString("sessions") }

// Web is the root folder of the served web content.
func Web() string { return viper.GetString("web") }

// WebHost is the host to bind the web server to.
func WebHost() string { return viper.GetString("host") }

// WebPort is the port to bind the web server to.
func WebPort() uint { return uint(viper.GetInt32("port")) }

// MailHost is the server of the SMTP server.
func MailHost() string { return viper.GetString("mail-host") }

// MailPort is the port of the SMTP server.
func MailPort() uint { return uint(viper.GetInt32("mail-port")) }

// MailUser is the user to connect to the SMTP server.
func MailUser() string { return viper.GetString("mail-user") }

// MailPassword is the password of the user to connect to the SMTP server.
func MailPassword() string { return viper.GetString("mail-password") }

// MailFrom is the sender in the 'from' header in mails send.
func MailFrom() string { return viper.GetString("mail-from") }

// MailHelo is the helo message send to the SMTP server.
func MailHelo() string { return viper.GetString("mail-helo") }

// AllowedOrigins is a list of allowed host for CORS headers.
func AllowedOrigins() []string { return viper.GetStringSlice("allowed-origins") }

// GeoServerURL is the URL of the GeoServer used by gemma to serve map data.
func GeoServerURL() string { return viper.GetString("geoserver-url") }

// GeoServerUser is the adminstrative user to connect to the GeoServer.
func GeoServerUser() string { return viper.GetString("geoserver-user") }

// GeoServerPassword is the password of the adminstrative user
// to connect to the GeoServer.
func GeoServerPassword() string { return viper.GetString("geoserver-password") }

// GeoServerClean is a flag to indicate that the GeoServer setup for
// gemma should be freshly created at boot time.
// This is useful in case of gemma update.
// If false the only missing parts are added to the GeoServer setup.
// This should be the default mode when running gemma after an update
// as it reduces the pressure on the GeoServer and enables faster
// rebooting.
func GeoServerClean() bool { return viper.GetBool("geoserver-clean") }

// GeoServerStartupSQL can be used to add SQL commands to GeoServers database
// session start script. See
// https://docs.geoserver.org/stable/en/user/data/database/sqlsession.html
// Note that the commands will be executed as the user configured as 'db-user'.
func GeoServerStartupSQL() string {
	return viper.GetString("geoserver-startup-sql")
}

// TmpDir is the path where to store temporary files.
// If left empty the system default for temporary files is used.
func TmpDir() string { return viper.GetString("tmp-dir") }

// PublishedConfig is a name of a JSON file where extra configuration is stored
// to be served to to the web client.
func PublishedConfig() string { return viper.GetString("published-config") }

// SOAPTimeout is the timeout till a SOAP request is canceled.
func SOAPTimeout() time.Duration { return viper.GetDuration("soap-timeout") }

// ReportPath is a path where report templates are stored.
func ReportPath() string { return viper.GetString("report-path") }

// LogFile is the path to the log file.
func LogFile() string { return viper.GetString("log-file") }

// LogLevel is the log level of the application.
func LogLevel() log.Level { return log.ParseLogLevel(viper.GetString("log-level")) }

// TokenURL is the ERDMS token service URL.
func TokenURL() string { return viper.GetString("token-url") }

var (
	proxyKeyOnce       sync.Once
	proxyKey           []byte
	proxyPrefixOnce    sync.Once
	proxyPrefix        string
	externalURLOnce    sync.Once
	externalURL        string
	sessionTimeoutOnce sync.Once
	sessionTimeout     time.Duration
)

// ProxyKey is a crytographic key to sign the URLs generated by the proxy.
// Use this to ensure that only known URLs are reachable over the proxy.
// If left blank a random key is generated a gemma boot time.
// Setting this value in the configuration will allow browsing proxy
// generated URL across gemma reboot.
// Use a strong secret key here (like pwgen -s 20).
func ProxyKey() []byte {
	fetchKey := func() {
		if proxyKey == nil {
			key := []byte(viper.GetString("proxy-key"))
			if len(key) == 0 {
				key = common.GenerateRandomKey(64)
			}
			hash := sha256.New()
			hash.Write(key)
			proxyKey = hash.Sum(nil)
		}
	}
	proxyKeyOnce.Do(fetchKey)
	return proxyKey
}

// ProxyPrefix is the prefix used in generated URLs by the proxy.
// You may need to set this if you run gemma behind a proxy
// on a specific domain.
func ProxyPrefix() string {
	fetchPrefix := func() {
		if proxyPrefix == "" {
			if proxyPrefix = viper.GetString("proxy-prefix"); proxyPrefix == "" {
				proxyPrefix = fmt.Sprintf("http://%s:%d", WebHost(), WebPort())
			}
		}
	}
	proxyPrefixOnce.Do(fetchPrefix)
	return proxyPrefix
}

// ExternalURL is the URL to find this server from the outside.
func ExternalURL() string {
	fetchExternal := func() {
		if externalURL == "" {
			if externalURL = viper.GetString("external-url"); externalURL == "" {
				externalURL = fmt.Sprintf("http://%s:%d", WebHost(), WebPort())
			}
		}
	}
	externalURLOnce.Do(fetchExternal)
	return externalURL
}

// SessionTimeout is the duration until a session expires if not renewed
func SessionTimeout() time.Duration {
	fetchTimeout := func() {
		if sessionTimeout == 0 {
			sessionTimeout = viper.GetDuration("session-timeout")
			if sessionTimeout <= 0 {
				lg.Println("warn: non-positive session-timeout configured.")
			}
		}
	}
	sessionTimeoutOnce.Do(fetchTimeout)
	return sessionTimeout
}

// SchemaDirs are the root directories where to find schema files.
func SchemaDirs() string { return viper.GetString("schema-dirs") }

// RootCmd is cobra command to be bound th the cobra/viper infrastructure.
var RootCmd = &cobra.Command{
	Use:   "gemma",
	Short: "gemma is a server for waterway monitoring and management",
}

var allowedOrigins = []string{
	// TODO: Fill me!
}

func init() {
	cobra.OnInitialize(initConfig)
	fl := RootCmd.PersistentFlags()
	fl.StringVarP(&configFile, "config", "c", "", "config file (default is $HOME/.gemma.toml)")

	vbind := func(name string) { viper.BindPFlag(name, fl.Lookup(name)) }

	str := func(name, value, usage string) {
		fl.String(name, value, usage)
		vbind(name)
	}
	strP := func(name, shorthand, value, usage string) {
		fl.StringP(name, shorthand, value, usage)
		vbind(name)
	}
	ui := func(name string, value uint, usage string) {
		fl.Uint(name, value, usage)
		vbind(name)
	}
	uiP := func(name, shorthand string, value uint, usage string) {
		fl.UintP(name, shorthand, value, usage)
		vbind(name)
	}
	strSl := func(name string, value []string, usage string) {
		fl.StringSlice(name, value, usage)
		vbind(name)
	}
	bl := func(name string, value bool, usage string) {
		fl.Bool(name, value, usage)
		vbind(name)
	}
	d := func(name string, value time.Duration, usage string) {
		fl.Duration(name, value, usage)
		vbind(name)
	}

	strP("db-host", "H", "localhost", "host of the database")
	uiP("db-port", "P", 5432, "port of the database")
	strP("db-name", "d", "gemma", "name of the database")
	str("db-user", "meta_login", "Metamorphic database user")
	str("db-password", "", "Metamorphic database user password")
	strP("db-ssl", "S", "prefer", "SSL mode of the database")

	strP("sessions", "s", "", "path to the sessions file")
	d("session-timeout", 3*time.Hour, "duration until sessions expire")

	strP("web", "w", "./web", "path to the web files")
	strP("host", "o", "localhost", "host of the web app")
	uiP("port", "p", 8000, "port of the web app")

	str("mail-host", "localhost", "server to send mail with")
	ui("mail-port", 465, "port of server to send mail with")
	str("mail-user", "gemma", "user to authenticate against mail-host.\n"+
		"Leave empty for trying to send without auth.")
	str("mail-password", "", "password of user to send mail with")
	str("mail-from", "noreply@localhost", "from line of mails")
	str("mail-helo", "localhost", "name of server to send mail from.")

	strSl("allowed-origins", allowedOrigins, "allow access for remote origins")

	str("geoserver-url", "http://localhost:8080/geoserver", "URL to GeoServer")
	str("geoserver-user", "admin", "GeoServer user")
	str("geoserver-password", "geoserver", "GeoServer password")
	bl("geoserver-clean", false, "Clean GeoServer setup")

	str("proxy-key", "", "signing key for proxy URLs.\n"+
		"Defaults to random key.")
	str("proxy-prefix", "", "URL prefix of proxy.\n"+
		"Defaults to 'http://${host}:${port}'")

	str("external-url", "", "URL to find the server from the outside.\n"+
		"Defaults to 'http://${host}:${port}'")

	str("tmp-dir", "", "Temp directory of gemma server.\n"+
		"Defaults to system temp directory.")

	str("schema-dirs", ".", "Directories to find XSD schema files in (recursive).")

	str("published-config", "", "path to a config file served to client.")

	str("report-path", "", "path to a report templates.")

	d("soap-timeout", 3*time.Minute, "Timeout till a SOAP request is canceled.")

	str("log-file", "", "path to a file to log to.")
	str("log-level", log.InfoLogLevel.String(), "path to a file to log to.")

	str("token-url", "", "URL to the ERDMS token server.")
}

var (
	configCond  = sync.NewCond(new(sync.Mutex))
	configReady bool
)

// Ready tells if the configuration is ready to use..
func Ready() {
	configCond.L.Lock()
	defer configCond.L.Unlock()
	configReady = true
	configCond.Broadcast()
}

// WaitReady blocks until the configuration is ready to use.
// Call this if you have a go routine that needs configuration
// support. This guarantees that the initialization is done
// before accessing the configuration data.
func WaitReady() {
	configCond.L.Lock()
	defer configCond.L.Unlock()
	for !configReady {
		configCond.Wait()
	}
}

func initConfig() {
	// Don't forget to read config either from cfgFile or from home directory!
	if configFile != "" {
		// Use config file from the flag.
		viper.SetConfigFile(configFile)
	} else {
		// Find home directory.
		home, err := homedir.Dir()
		if err != nil {
			lg.Fatalf("error: %v\n", err)
		}

		// Search config in home directory with name ".cobra" (without extension).
		viper.AddConfigPath(home)
		viper.SetConfigName(".gemma")
	}
	if err := viper.ReadInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); ok && configFile == "" {
			// Don't bother if not found.
			return
		}
		lg.Fatalf("Can't read config: %v\n", err)
	}
}