view pkg/config/config.go @ 1644:eadf84bb0e98

New config variable 'external-url'. Deep inside the import queue we don't known the URL we find the server at. We don't have any HTTP request we can derive this information wrong so it needs to be configured. Defaults to http://${web-host}:${web-port} .
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Thu, 20 Dec 2018 14:39:23 +0100
parents a25a4d4a3e6e
children bd09d6ad4c14
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 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

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

	homedir "github.com/mitchellh/go-homedir"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"

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

// 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") }

// 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") }

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

// 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.
// It defauls to http://${WebHost}:${WebPort}".
// 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.
// It defauls to http://${WebHost}:${WebPort}".
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
}

// 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)
	}

	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")

	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://${web-host}:${web-port}'")

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

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

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 {
			log.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
		}
		log.Fatalf("Can't read config: %v\n", err)
	}
}