static-file-server/config/config.go

292 lines
7.7 KiB
Go

package config
import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"strconv"
"strings"
yaml "gopkg.in/yaml.v2"
)
var (
// Get the desired configuration value.
Get struct {
Cors bool `yaml:"cors"`
Debug bool `yaml:"debug"`
Folder string `yaml:"folder"`
Host string `yaml:"host"`
Port uint16 `yaml:"port"`
ShowListing bool `yaml:"show-listing"`
TLSCert string `yaml:"tls-cert"`
TLSKey string `yaml:"tls-key"`
TLSMinVers uint16 `yaml:"-"`
TLSMinVersStr string `yaml:"tls-min-vers"`
URLPrefix string `yaml:"url-prefix"`
Referrers []string `yaml:"referrers"`
}
)
const (
corsKey = "CORS"
debugKey = "DEBUG"
folderKey = "FOLDER"
hostKey = "HOST"
portKey = "PORT"
referrersKey = "REFERRERS"
showListingKey = "SHOW_LISTING"
tlsCertKey = "TLS_CERT"
tlsKeyKey = "TLS_KEY"
tlsMinVersKey = "TLS_MIN_VERS"
urlPrefixKey = "URL_PREFIX"
)
var (
defaultDebug = false
defaultFolder = "/web"
defaultHost = ""
defaultPort = uint16(8080)
defaultReferrers = []string{}
defaultShowListing = true
defaultTLSCert = ""
defaultTLSKey = ""
defaultTLSMinVers = ""
defaultURLPrefix = ""
defaultCors = false
)
func init() {
// init calls setDefaults to better support testing.
setDefaults()
}
func setDefaults() {
Get.Debug = defaultDebug
Get.Folder = defaultFolder
Get.Host = defaultHost
Get.Port = defaultPort
Get.Referrers = defaultReferrers
Get.ShowListing = defaultShowListing
Get.TLSCert = defaultTLSCert
Get.TLSKey = defaultTLSKey
Get.TLSMinVersStr = defaultTLSMinVers
Get.URLPrefix = defaultURLPrefix
Get.Cors = defaultCors
}
// Load the configuration file.
func Load(filename string) (err error) {
// If no filename provided, assign envvars.
if filename == "" {
overrideWithEnvVars()
return validate()
}
// Read contents from configuration file.
var contents []byte
if contents, err = ioutil.ReadFile(filename); nil != err {
return
}
// Parse contents into 'Get' configuration.
if err = yaml.Unmarshal(contents, &Get); nil != err {
return
}
overrideWithEnvVars()
return validate()
}
// Log the current configuration.
func Log() {
// YAML marshalling should never error, but if it could, the result is that
// the contents of the configuration are not logged.
contents, _ := yaml.Marshal(&Get)
// Log the configuration.
fmt.Println("Using the following configuration:")
fmt.Println(string(contents))
}
// overrideWithEnvVars the default values and the configuration file values.
func overrideWithEnvVars() {
// Assign envvars, if set.
Get.Cors = envAsBool(corsKey, Get.Cors)
Get.Debug = envAsBool(debugKey, Get.Debug)
Get.Folder = envAsStr(folderKey, Get.Folder)
Get.Host = envAsStr(hostKey, Get.Host)
Get.Port = envAsUint16(portKey, Get.Port)
Get.ShowListing = envAsBool(showListingKey, Get.ShowListing)
Get.TLSCert = envAsStr(tlsCertKey, Get.TLSCert)
Get.TLSKey = envAsStr(tlsKeyKey, Get.TLSKey)
Get.TLSMinVersStr = envAsStr(tlsMinVersKey, Get.TLSMinVersStr)
Get.URLPrefix = envAsStr(urlPrefixKey, Get.URLPrefix)
Get.Referrers = envAsStrSlice(referrersKey, Get.Referrers)
}
// validate the configuration.
func validate() error {
// If HTTPS is to be used, verify both TLS_* environment variables are set.
useTLS := false
if 0 < len(Get.TLSCert) || 0 < len(Get.TLSKey) {
if len(Get.TLSCert) == 0 || len(Get.TLSKey) == 0 {
msg := "if value for either 'TLS_CERT' or 'TLS_KEY' is set then " +
"then value for the other must also be set (values are " +
"currently '%s' and '%s', respectively)"
return fmt.Errorf(msg, Get.TLSCert, Get.TLSKey)
}
if _, err := os.Stat(Get.TLSCert); nil != err {
msg := "value of TLS_CERT is set with filename '%s' that returns %v"
return fmt.Errorf(msg, Get.TLSCert, err)
}
if _, err := os.Stat(Get.TLSKey); nil != err {
msg := "value of TLS_KEY is set with filename '%s' that returns %v"
return fmt.Errorf(msg, Get.TLSKey, err)
}
useTLS = true
}
// Verify TLS_MIN_VERS is only (optionally) set if TLS is to be used.
Get.TLSMinVers = tls.VersionTLS10
if useTLS {
if 0 < len(Get.TLSMinVersStr) {
var err error
if Get.TLSMinVers, err = tlsMinVersAsUint16(
Get.TLSMinVersStr,
); nil != err {
return err
}
}
// For logging minimum TLS version being used while debugging, backfill
// the TLSMinVersStr field.
switch Get.TLSMinVers {
case tls.VersionTLS10:
Get.TLSMinVersStr = "TLS1.0"
case tls.VersionTLS11:
Get.TLSMinVersStr = "TLS1.1"
case tls.VersionTLS12:
Get.TLSMinVersStr = "TLS1.2"
case tls.VersionTLS13:
Get.TLSMinVersStr = "TLS1.3"
}
} else {
if 0 < len(Get.TLSMinVersStr) {
msg := "value for 'TLS_MIN_VERS' is set but 'TLS_CERT' and 'TLS_KEY' are not"
return errors.New(msg)
}
}
// If the URL path prefix is to be used, verify it is properly formatted.
if 0 < len(Get.URLPrefix) &&
(!strings.HasPrefix(Get.URLPrefix, "/") || strings.HasSuffix(Get.URLPrefix, "/")) {
msg := "if value for 'URL_PREFIX' is set then the value must start " +
"with '/' and not end with '/' (current value of '%s' vs valid " +
"example of '/my/prefix'"
return fmt.Errorf(msg, Get.URLPrefix)
}
return nil
}
// envAsStr returns the value of the environment variable as a string if set.
func envAsStr(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
// envAsStrSlice returns the value of the environment variable as a slice of
// strings if set.
func envAsStrSlice(key string, fallback []string) []string {
if value := os.Getenv(key); value != "" {
return strings.Split(value, ",")
}
return fallback
}
// envAsUint16 returns the value of the environment variable as a uint16 if set.
func envAsUint16(key string, fallback uint16) uint16 {
// Retrieve the string value of the environment variable. If not set,
// fallback is used.
valueStr := os.Getenv(key)
if valueStr == "" {
return fallback
}
// Parse the string into a uint16.
base := 10
bitSize := 16
valueAsUint64, err := strconv.ParseUint(valueStr, base, bitSize)
if nil != err {
log.Printf(
"Invalid value for '%s': %v\nUsing fallback: %d",
key, err, fallback,
)
return fallback
}
return uint16(valueAsUint64)
}
// envAsBool returns the value for an environment variable or, if not set, a
// fallback value as a boolean.
func envAsBool(key string, fallback bool) bool {
// Retrieve the string value of the environment variable. If not set,
// fallback is used.
valueStr := os.Getenv(key)
if valueStr == "" {
return fallback
}
// Parse the string into a boolean.
value, err := strAsBool(valueStr)
if nil != err {
log.Printf(
"Invalid value for '%s': %v\nUsing fallback: %t",
key, err, fallback,
)
return fallback
}
return value
}
// strAsBool converts the intent of the passed value into a boolean
// representation.
func strAsBool(value string) (result bool, err error) {
lvalue := strings.ToLower(value)
switch lvalue {
case "0", "false", "f", "no", "n":
result = false
case "1", "true", "t", "yes", "y":
result = true
default:
result = false
msg := "unknown conversion from string to bool for value '%s'"
err = fmt.Errorf(msg, value)
}
return
}
// tlsMinVersAsUint16 converts the intent of the passed value into an
// enumeration for the crypto/tls package.
func tlsMinVersAsUint16(value string) (result uint16, err error) {
switch strings.ToLower(value) {
case "tls10":
result = tls.VersionTLS10
case "tls11":
result = tls.VersionTLS11
case "tls12":
result = tls.VersionTLS12
case "tls13":
result = tls.VersionTLS13
default:
err = fmt.Errorf("unknown value for TLS_MIN_VERS: %s", value)
}
return
}