refactor: code cleanup, stricter config validation (#155)

This commit is contained in:
Henrique Dias
2024-07-21 20:52:50 +02:00
committed by GitHub
parent 46d54e4465
commit c125bedae1
19 changed files with 683 additions and 654 deletions
-224
View File
@@ -1,224 +0,0 @@
package cmd
import (
"errors"
"log"
"os"
"regexp"
"strconv"
"strings"
"github.com/hacdias/webdav/v4/lib"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
"golang.org/x/net/webdav"
)
func parseRules(raw []interface{}, defaultModify bool) []*lib.Rule {
rules := []*lib.Rule{}
for _, v := range raw {
if r, ok := v.(map[interface{}]interface{}); ok {
rule := &lib.Rule{
Regex: false,
Allow: false,
Modify: defaultModify,
Path: "",
}
if regex, ok := r["regex"].(bool); ok {
rule.Regex = regex
}
if allow, ok := r["allow"].(bool); ok {
rule.Allow = allow
}
if modify, ok := r["modify"].(bool); ok {
rule.Modify = modify
if modify {
rule.Allow = true
}
}
path, ok := r["path"].(string)
if !ok {
continue
}
if rule.Regex {
rule.Regexp = regexp.MustCompile(path)
} else {
rule.Path = path
}
rules = append(rules, rule)
}
}
return rules
}
func loadFromEnv(v string) (string, error) {
v = strings.TrimPrefix(v, "{env}")
if v == "" {
return "", errors.New("no environment variable specified")
}
v = os.Getenv(v)
if v == "" {
return "", errors.New("the environment variable is empty")
}
return v, nil
}
func parseUsers(raw []interface{}, c *lib.Config) {
var err error
for _, v := range raw {
if u, ok := v.(map[interface{}]interface{}); ok {
username, ok := u["username"].(string)
if !ok {
log.Fatal("user needs an username")
}
if strings.HasPrefix(username, "{env}") {
username, err = loadFromEnv(username)
checkErr(err)
}
password, ok := u["password"].(string)
if !ok {
password = ""
if numPwd, ok := u["password"].(int); ok {
password = strconv.Itoa(numPwd)
}
}
if strings.HasPrefix(password, "{env}") {
password, err = loadFromEnv(password)
checkErr(err)
}
user := &lib.User{
Username: username,
Password: password,
Scope: c.User.Scope,
Modify: c.User.Modify,
Rules: c.User.Rules,
}
if scope, ok := u["scope"].(string); ok {
user.Scope = scope
}
if modify, ok := u["modify"].(bool); ok {
user.Modify = modify
}
if rules, ok := u["rules"].([]interface{}); ok {
user.Rules = append(c.User.Rules, parseRules(rules, user.Modify)...)
}
user.Handler = &webdav.Handler{
Prefix: c.User.Handler.Prefix,
FileSystem: lib.WebDavDir{
Dir: webdav.Dir(user.Scope),
NoSniff: c.NoSniff,
},
LockSystem: webdav.NewMemLS(),
}
c.Users[username] = user
}
}
}
func parseCors(cfg map[string]interface{}, c *lib.Config) {
cors := lib.CorsCfg{
Enabled: cfg["enabled"].(bool),
Credentials: cfg["credentials"].(bool),
}
cors.AllowedHeaders = corsProperty("allowed_headers", cfg)
cors.AllowedHosts = corsProperty("allowed_hosts", cfg)
cors.AllowedMethods = corsProperty("allowed_methods", cfg)
cors.ExposedHeaders = corsProperty("exposed_headers", cfg)
c.Cors = cors
}
func corsProperty(property string, cfg map[string]interface{}) []string {
var def []string
if property == "exposed_headers" {
def = []string{}
} else {
def = []string{"*"}
}
if allowed, ok := cfg[property].([]interface{}); ok {
items := make([]string, len(allowed))
for idx, a := range allowed {
items[idx] = a.(string)
}
if len(items) == 0 {
return def
}
return items
}
return def
}
func readConfig(flags *pflag.FlagSet) *lib.Config {
cfg := &lib.Config{
User: &lib.User{
Scope: getOpt(flags, "scope"),
Modify: getOptB(flags, "modify"),
Rules: []*lib.Rule{},
Handler: &webdav.Handler{
Prefix: getOpt(flags, "prefix"),
FileSystem: lib.WebDavDir{
Dir: webdav.Dir(getOpt(flags, "scope")),
NoSniff: getOptB(flags, "nosniff"),
},
LockSystem: webdav.NewMemLS(),
},
},
Debug: getOptB(flags, "debug"),
Auth: getOptB(flags, "auth"),
NoSniff: getOptB(flags, "nosniff"),
Cors: lib.CorsCfg{
Enabled: false,
Credentials: false,
},
Users: map[string]*lib.User{},
LogFormat: getOpt(flags, "log_format"),
}
rawRules := v.Get("rules")
if rules, ok := rawRules.([]interface{}); ok {
cfg.User.Rules = parseRules(rules, cfg.User.Modify)
}
rawUsers := v.Get("users")
if users, ok := rawUsers.([]interface{}); ok {
parseUsers(users, cfg)
}
rawCors := v.Get("cors")
if cors, ok := rawCors.(map[string]interface{}); ok {
parseCors(cors, cfg)
}
if len(cfg.Users) != 0 && !cfg.Auth {
log.Print("Users will be ignored due to auth=false")
}
return cfg
}
+84 -82
View File
@@ -1,7 +1,8 @@
package cmd
import (
"log"
"errors"
"fmt"
"net"
"net/http"
"os"
@@ -9,27 +10,21 @@ import (
"strings"
"syscall"
"github.com/hacdias/webdav/v4/lib"
"github.com/spf13/cobra"
v "github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var (
cfgFile string
)
func init() {
cobra.OnInitialize(initConfig)
flags := rootCmd.Flags()
flags.StringVarP(&cfgFile, "config", "c", "", "config file path")
flags.BoolP("tls", "t", false, "enable tls")
flags.Bool("auth", true, "enable auth")
flags.String("cert", "cert.pem", "TLS certificate")
flags.String("key", "key.pem", "TLS key")
flags.StringP("address", "a", "0.0.0.0", "address to listen to")
flags.StringP("port", "p", "0", "port to listen to")
flags.StringP("config", "c", "", "config file path")
flags.BoolP("tls", "t", false, "enable TLS")
flags.Bool("auth", false, "enable authentication")
flags.String("cert", "cert.pem", "path to TLS certificate")
flags.String("key", "key.pem", "path to TLS key")
flags.StringP("address", "a", "0.0.0.0", "address to listen on")
flags.StringP("port", "p", "0", "port to listen on")
flags.StringP("prefix", "P", "/", "URL path prefix")
flags.String("log_format", "console", "logging format")
}
@@ -53,91 +48,98 @@ The precedence of the configuration values are as follows:
The environment variables are prefixed by "WD_" followed by the option
name in caps. So to set "cert" via an env variable, you should
set WD_CERT.`,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()
cfg := readConfig(flags)
// Build address and listener
laddr := getOpt(flags, "address")
var lnet string
if strings.HasPrefix(laddr, "unix:") {
laddr = laddr[5:]
lnet = "unix"
} else {
laddr = laddr + ":" + getOpt(flags, "port")
lnet = "tcp"
}
listener, err := net.Listen(lnet, laddr)
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go func(c chan os.Signal) {
// Wait for a SIGINT or SIGKILL:
sig := <-c
log.Printf("Caught signal %s: shutting down.", sig)
// Stop listening (and unlink the socket if unix type):
listener.Close()
// And we're done:
os.Exit(0)
}(sigc)
cfgFilename, _ := flags.GetString("config")
cfg, err := lib.ParseConfig(cfgFilename, flags)
if err != nil {
log.Fatal(err)
return err
}
loggerConfig := zap.NewProductionConfig()
loggerConfig.DisableCaller = true
if cfg.Debug {
loggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
}
loggerConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
loggerConfig.Encoding = cfg.LogFormat
logger, err := loggerConfig.Build()
// Create HTTP handler from the config
handler, err := lib.NewHandler(cfg)
if err != nil {
// if we fail to configure proper logging, then the user has deliberately
// misconfigured the logger. Abort.
panic(err)
return err
}
zap.ReplaceGlobals(logger)
// Setup the logger based on the configuration
err = setupLogger(cfg)
if err != nil {
return err
}
defer func() {
// Flush the logger at the end
_ = zap.L().Sync()
}()
// Tell the user the port in which is listening.
zap.L().Info("Listening", zap.String("address", listener.Addr().String()))
// Starts the server.
if getOptB(flags, "tls") {
if err := http.ServeTLS(listener, cfg, getOpt(flags, "cert"), getOpt(flags, "key")); err != nil {
zap.L().Fatal("shutting server", zap.Error(err))
}
} else {
if err := http.Serve(listener, cfg); err != nil {
zap.L().Fatal("shutting server", zap.Error(err))
}
// Build listener
listener, err := getListener(cfg)
if err != nil {
return err
}
// Trap exiting signals
quit := make(chan os.Signal, 1)
go func() {
zap.L().Info("listening", zap.String("address", listener.Addr().String()))
var err error
if cfg.TLS {
err = http.ServeTLS(listener, handler, cfg.Cert, cfg.Key)
} else {
err = http.Serve(listener, handler)
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
zap.L().Error("failed to start server", zap.Error(err))
}
quit <- os.Interrupt
}()
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
signal := <-quit
zap.L().Info("caught signal, shutting down", zap.Stringer("signal", signal))
_ = listener.Close()
return nil
},
}
func initConfig() {
if cfgFile == "" {
v.AddConfigPath(".")
v.AddConfigPath("/etc/webdav/")
v.SetConfigName("config")
func getListener(cfg *lib.Config) (net.Listener, error) {
var (
address string
network string
)
if strings.HasPrefix(cfg.Address, "unix:") {
address = cfg.Address[5:]
network = "unix"
} else {
v.SetConfigFile(cfgFile)
address = fmt.Sprintf("%s:%d", cfg.Address, cfg.Port)
network = "tcp"
}
v.SetEnvPrefix("WD")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
return net.Listen(network, address)
}
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(v.ConfigParseError); ok {
panic(err)
}
cfgFile = "No config file used"
} else {
cfgFile = "Using config file: " + v.ConfigFileUsed()
func setupLogger(cfg *lib.Config) error {
loggerConfig := zap.NewProductionConfig()
loggerConfig.DisableCaller = true
if cfg.Debug {
loggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
}
loggerConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
loggerConfig.Encoding = cfg.LogFormat
logger, err := loggerConfig.Build()
if err != nil {
return err
}
zap.ReplaceGlobals(logger)
return nil
}
+1 -1
View File
@@ -13,7 +13,7 @@ func init() {
Use: "version",
Short: "Print the version number",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("WebDAV version: %q", version)
fmt.Printf("WebDAV version: %s\n", version)
},
})
}
-55
View File
@@ -1,55 +0,0 @@
package cmd
import (
"log"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
)
// getOption returns a parameter as a string.
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getOpt(flags *pflag.FlagSet, key string) string {
value, _ := flags.GetString(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetString(key)
}
// Otherwise use default value on flags.
return value
}
func getOptB(flags *pflag.FlagSet, key string) bool {
value, _ := flags.GetBool(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetBool(key)
}
// Otherwise use default value on flags.
return value
}
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}