getwtxt/getwtxt

View on GitHub
svc/conf.go

Summary

Maintainability
A
0 mins
Test Coverage
/*
Copyright (c) 2019 Ben Morrison (gbmor)

This file is part of Getwtxt.

Getwtxt is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Getwtxt is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Getwtxt.  If not, see <https://www.gnu.org/licenses/>.
*/

package svc // import "git.sr.ht/~gbmor/getwtxt/svc"

import (
    "fmt"
    "log"
    "os"
    "path/filepath"
    "strings"
    "sync"
    "time"

    "github.com/spf13/viper"
)

var reqLog *log.Logger

// Configuration values are held in an instance of
// this struct.
type Configuration struct {
    Mu            sync.RWMutex
    Port          int           `yaml:"ListenPort"`
    MsgLog        string        `yaml:"MessageLog"`
    ReqLog        string        `yaml:"RequestLog"`
    DBType        string        `yaml:"DatabaseType"`
    DBPath        string        `yaml:"DatabasePath"`
    AssetsDir     string        `yaml:"AssetsDirectory"`
    StaticDir     string        `yaml:"StaticFilesDirectory"`
    AdminPassHash string        `yaml:"-"`
    StdoutLogging bool          `yaml:"StdoutLogging"`
    CacheInterval time.Duration `yaml:"StatusFetchInterval"`
    DBInterval    time.Duration `yaml:"DatabasePushInterval"`
    Instance      `yaml:"Instance"`
}

// Instance refers to meta data about
// this specific instance of getwtxt
type Instance struct {
    Vers  string `yaml:"-"`
    Name  string `yaml:"Instance.SiteName"`
    URL   string `yaml:"Instance.URL"`
    Owner string `yaml:"Instance.OwnerName"`
    Mail  string `yaml:"Instance.Email"`
    Desc  string `yaml:"Instance.Description"`
}

// Called on start-up. Initializes everything
// related to configuration values.
func initConfig() {
    log.Printf("Loading configuration ...\n")

    parseConfigFlag()
    setConfigDefaults()

    if err := viper.ReadInConfig(); err != nil {
        log.Printf("%v\n", err.Error())
        log.Printf("Using defaults ...\n")
        bindConfig()
        return
    }

    viper.WatchConfig()
    viper.OnConfigChange(reInit)
    bindConfig()
}

// Registers either stdout or a specified file
// to the default logger, and the same for the
// request logger.
func initLogging() {
    confObj.Mu.RLock()
    defer confObj.Mu.RUnlock()

    if confObj.StdoutLogging {
        log.SetOutput(os.Stdout)
        reqLog = log.New(os.Stdout, "", log.LstdFlags)

    } else {
        msgLog, err := os.OpenFile(confObj.MsgLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
        errLog("Could not open log file: ", err)
        reqLogFile, err := os.OpenFile(confObj.ReqLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
        errLog("Could not open log file: ", err)

        // Listen for the signal to close the log file
        // in a separate thread. Passing it as an argument
        // to prevent race conditions when the config is
        // reloaded.
        go func(msg *os.File, req *os.File) {
            <-closeLog
            log.Printf("Closing log files ...\n\n")
            errLog("Could not close log file: ", msg.Close())
            errLog("Could not close log file: ", req.Close())
        }(msgLog, reqLogFile)

        log.SetOutput(msgLog)
        reqLog = log.New(reqLogFile, "", log.LstdFlags)
    }
}

// Default values should a config file
// not be available.
func setConfigDefaults() {
    viper.SetDefault("ListenPort", 9001)
    viper.SetDefault("MessageLog", "logs/message.log")
    viper.SetDefault("RequestLog", "logs/request.log")
    viper.SetDefault("DatabasePath", "getwtxt.db")
    viper.SetDefault("AssetsDirectory", "assets")
    viper.SetDefault("StaticFilesDirectory", "static")
    viper.SetDefault("DatabaseType", "leveldb")
    viper.SetDefault("StdoutLogging", false)
    viper.SetDefault("ReCacheInterval", "1h")
    viper.SetDefault("DatabasePushInterval", "5m")
    viper.SetDefault("AdminPassword", "please_change_me")

    viper.SetDefault("Instance.SiteName", "getwtxt")
    viper.SetDefault("Instance.OwnerName", "Anonymous Microblogger")
    viper.SetDefault("Instance.Email", "nobody@knows")
    viper.SetDefault("Instance.URL", "https://twtxt.example.com")
    viper.SetDefault("Instance.Description", "A fast, resilient twtxt registry server written in Go!")
}

// Reads data from the configuration
// flag and acts accordingly.
func parseConfigFlag() {
    if *flagConfFile == "" {
        viper.SetConfigName("getwtxt")
        viper.SetConfigType("yml")
        viper.AddConfigPath(".")
        viper.AddConfigPath("/usr/local/getwtxt")
        viper.AddConfigPath("/etc")
        viper.AddConfigPath("/usr/local/etc")

    } else {
        path, file := filepath.Split(*flagConfFile)
        if path == "" {
            path = "."
        }
        filename := strings.Split(file, ".")
        viper.SetConfigName(filename[0])
        viper.SetConfigType(filename[1])
        viper.AddConfigPath(path)
    }
}

// Simply goes down the list of fields
// in the confObj instance of &Configuration{},
// assigning values from the config file.
func bindConfig() {
    confObj.Mu.Lock()

    confObj.Port = viper.GetInt("ListenPort")
    confObj.MsgLog = viper.GetString("MessageLog")
    confObj.ReqLog = viper.GetString("RequestLog")
    confObj.DBType = strings.ToLower(viper.GetString("DatabaseType"))
    confObj.DBPath = viper.GetString("DatabasePath")
    confObj.AssetsDir = viper.GetString("AssetsDirectory")
    confObj.StaticDir = viper.GetString("StaticFilesDirectory")
    confObj.StdoutLogging = viper.GetBool("StdoutLogging")
    confObj.CacheInterval = viper.GetDuration("StatusFetchInterval")
    confObj.DBInterval = viper.GetDuration("DatabasePushInterval")
    txtPass := viper.GetString("AdminPassword")
    if txtPass == "please_change_me" || strings.TrimSpace(txtPass) == "" {
        fmt.Println("Please set AdminPassword in getwtxt.yml")
        os.Exit(1)
    }
    passHash, err := HashPass(txtPass)
    if err != nil {
        errFatal("Failed to hash administrator password: ", err)
    }
    confObj.AdminPassHash = passHash

    confObj.Instance.Vers = Vers
    confObj.Instance.Name = viper.GetString("Instance.SiteName")
    confObj.Instance.URL = viper.GetString("Instance.URL")
    confObj.Instance.Owner = viper.GetString("Instance.OwnerName")
    confObj.Instance.Mail = viper.GetString("Instance.Email")
    confObj.Instance.Desc = viper.GetString("Instance.Description")

    if *flagDBType != "" {
        confObj.DBType = *flagDBType
    }
    if *flagDBPath != "" {
        confObj.DBPath = *flagDBPath
    }
    if *flagAssets != "" {
        confObj.AssetsDir = *flagAssets
    }

    confObj.Mu.Unlock()
    announceConfig()
}

// Echoes chosen configuration options on startup and after
// a change in getwtxt.yml is detected.
func announceConfig() {
    confObj.Mu.RLock()
    defer confObj.Mu.RUnlock()

    if confObj.StdoutLogging {
        log.Printf("Logging to: stdout\n")
    } else {
        log.Printf("Logging messages to: %v\n", confObj.MsgLog)
        log.Printf("Logging requests to: %v\n", confObj.ReqLog)
    }
    log.Printf("Using %v database: %v\n", confObj.DBType, confObj.DBPath)
    log.Printf("Database push interval: %v\n", confObj.DBInterval)
    log.Printf("User status fetch interval: %v\n", confObj.CacheInterval)
    log.Printf("Static files directory: %v", confObj.StaticDir)
}