Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some Version 5 Featuers #166

Merged
merged 5 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /webdav/main /bin/webdav

EXPOSE 80
EXPOSE 6065

ENTRYPOINT [ "webdav" ]
CMD [ "-p", "80" ]
64 changes: 35 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ For usage information regarding the CLI, run `webdav --help`.

### Docker

To use with Docker, you need to provide a configuration file and mount the data directories. For example, let's take the following configuration file that simply sets the port to `6060` and the scope to `/data`.
To use with Docker, you need to provide a configuration file and mount the data directories. For example, let's take the following configuration file that simply sets the port to `6060` and the directory to `/data`.

```yaml
port: 6060
scope: /data
directory: /data
```

You can now run with the following Docker command, where you mount the configuration file inside the container, and the data directory too, as well as forwarding the port 6060. You will need to change this to match your own configuration.
Expand All @@ -55,7 +55,7 @@ The configuration can be provided as a YAML, JSON or TOML file. Below is an exam

```yaml
address: 0.0.0.0
port: 0
port: 6065

# TLS-related settings if you want to enable TLS directly.
tls: false
Expand All @@ -68,30 +68,51 @@ prefix: /
# Enable or disable debug logging. Default is false.
debug: false

# Whether or not to have authentication. With authentication on, you need to
# define one or more users. Default is false.
auth: true

# The directory that will be able to be accessed by the users when connecting.
# This directory will be used by users unless they have their own 'scope' defined.
# Default is "/".
scope: /
# This directory will be used by users unless they have their own 'directory' defined.
# Default is "." (current directory).
directory: .

# Whether the users can, by default, modify the contents. Default is false.
modify: true

# Default permissions rules to apply at the paths.
rules: []

# The list of users. Must be defined if auth is set to true.
# Logging configuration
log:
# Logging format ('console', 'json'). Default is 'console'.
format: console
# Enable or disable colors. Default is 'true'. Only applied if format is 'console'.
colors: true
# Logging outputs. You can have more than one output. Default is only 'stderr'.
outputs:
- stderr

# CORS configuration
cors:
# Whether or not CORS configuration should be applied. Default is 'false'.
enabled: true
credentials: true
allowed_headers:
- Depth
allowed_hosts:
- http://localhost:8080
allowed_methods:
- GET
exposed_headers:
- Content-Length
- Content-Range

# The list of users. If users is empty, then there will be no authentication.
users:
# Example 'admin' user with plaintext password.
- username: admin
password: admin
# Example 'john' user with bcrypt encrypted password, with custom scope.
# Example 'john' user with bcrypt encrypted password, with custom directory.
- username: john
password: "{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi"
scope: /another/path
directory: /another/path
# Example user whose details will be picked up from the environment.
- username: "{env}ENV_USERNAME"
password: "{env}ENV_PASSWORD"
Expand All @@ -108,23 +129,8 @@ users:
modify: true
# With this rule, the user CAN modify all files ending with .js. It uses
# a regular expression.
- path: "^*.js$"
regex: true
- regex: "^.+\.js$"
modify: true

# CORS configuration
cors:
enabled: true
credentials: true
allowed_headers:
- Depth
allowed_hosts:
- http://localhost:8080
allowed_methods:
- GET
exposed_headers:
- Content-Length
- Content-Range
```

### CORS
Expand Down
32 changes: 7 additions & 25 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,17 @@ import (
"github.com/hacdias/webdav/v4/lib"
"github.com/spf13/cobra"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func init() {
flags := rootCmd.Flags()
flags.StringP("config", "c", "", "config file path")
flags.StringP("address", "a", lib.DefaultAddress, "address to listen on")
flags.IntP("port", "p", lib.DefaultPort, "port to listen on")
flags.BoolP("tls", "t", lib.DefaultTLS, "enable TLS")
flags.Bool("auth", lib.DefaultAuth, "enable authentication")
flags.String("cert", lib.DefaultCert, "path to TLS certificate")
flags.String("key", lib.DefaultKey, "path to TLS key")
flags.StringP("address", "a", lib.DefaultAddress, "address to listen on")
flags.IntP("port", "p", lib.DefaultPort, "port to listen on")
flags.StringP("prefix", "P", lib.DefaultPrefix, "URL path prefix")
flags.String("log_format", lib.DefaultLogFormat, "logging format")
}

var rootCmd = &cobra.Command{
Expand Down Expand Up @@ -58,14 +55,15 @@ set WD_CERT.`,
return err
}

// Create HTTP handler from the config
handler, err := lib.NewHandler(cfg)
// Setup the logger based on the configuration
logger, err := cfg.GetLogger()
if err != nil {
return err
}
zap.ReplaceGlobals(logger)

// Setup the logger based on the configuration
err = setupLogger(cfg)
// Create HTTP handler from the config
handler, err := lib.NewHandler(cfg)
if err != nil {
return err
}
Expand Down Expand Up @@ -127,19 +125,3 @@ func getListener(cfg *lib.Config) (net.Listener, error) {

return net.Listen(network, address)
}

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 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/hacdias/webdav/v4
go 1.22

require (
github.com/go-viper/mapstructure/v2 v2.0.0
github.com/rs/cors v1.11.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
Expand Down
83 changes: 46 additions & 37 deletions lib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,20 @@ import (
"path/filepath"
"strings"

"github.com/go-viper/mapstructure/v2"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

const (
DefaultScope = "/"
DefaultModify = false
DefaultDebug = false
DefaultNoSniff = false
DefaultTLS = false
DefaultAuth = false
DefaultCert = "cert.pem"
DefaultKey = "key.pem"
DefaultAddress = "0.0.0.0"
DefaultPort = 0
DefaultPrefix = "/"
DefaultLogFormat = "console"
DefaultTLS = false
DefaultCert = "cert.pem"
DefaultKey = "key.pem"
DefaultAddress = "0.0.0.0"
DefaultPort = 6065
DefaultPrefix = "/"
)

type Config struct {
Expand All @@ -35,8 +32,7 @@ type Config struct {
Key string
Prefix string
NoSniff bool
LogFormat string `mapstructure:"log_format"`
Auth bool
Log Log
CORS CORS
Users []User
}
Expand All @@ -50,11 +46,6 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
if err != nil {
return nil, err
}

err = v.BindPFlag("LogFormat", flags.Lookup("log_format"))
if err != nil {
return nil, err
}
}

// Configuration file settings
Expand All @@ -74,20 +65,21 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
// empty or false.

// Defaults shared with flags
v.SetDefault("Scope", DefaultScope)
v.SetDefault("Modify", DefaultModify)
v.SetDefault("Debug", DefaultDebug)
v.SetDefault("NoSniff", DefaultNoSniff)
v.SetDefault("TLS", DefaultTLS)
v.SetDefault("Cert", DefaultCert)
v.SetDefault("Key", DefaultKey)
v.SetDefault("Address", DefaultAddress)
v.SetDefault("Port", DefaultPort)
v.SetDefault("Auth", DefaultAuth)
v.SetDefault("Prefix", DefaultPrefix)
v.SetDefault("Log_Format", DefaultLogFormat)

// Other defaults
v.SetDefault("Directory", ".")
v.SetDefault("Modify", false)
v.SetDefault("Debug", false)
v.SetDefault("NoSniff", false)
v.SetDefault("Log.Format", "console")
v.SetDefault("Log.Outputs", []string{"stderr"})
v.SetDefault("Log.Colors", true)
v.SetDefault("CORS.Allowed_Headers", []string{"*"})
v.SetDefault("CORS.Allowed_Hosts", []string{"*"})
v.SetDefault("CORS.Allowed_Methods", []string{"*"})
Expand All @@ -101,15 +93,19 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
}

cfg := &Config{}
err = v.Unmarshal(cfg)
err = v.Unmarshal(cfg, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
mapstructure.TextUnmarshallerHookFunc(),
)))
if err != nil {
return nil, err
}

// Cascade user settings
for i := range cfg.Users {
if !v.IsSet(fmt.Sprintf("Users.%d.Scope", i)) {
cfg.Users[i].Scope = cfg.Scope
if !v.IsSet(fmt.Sprintf("Users.%d.Directory", i)) {
cfg.Users[i].Directory = cfg.Directory
}

if !v.IsSet(fmt.Sprintf("Users.%d.Modify", i)) {
Expand All @@ -132,15 +128,7 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
func (c *Config) Validate() error {
var err error

if c.Auth && len(c.Users) == 0 {
return errors.New("invalid config: auth cannot be enabled without users")
}

if !c.Auth && len(c.Users) != 0 {
return errors.New("invalid config: auth cannot be disabled with users defined")
}

c.Scope, err = filepath.Abs(c.Scope)
c.Directory, err = filepath.Abs(c.Directory)
if err != nil {
return fmt.Errorf("invalid config: %w", err)
}
Expand Down Expand Up @@ -180,6 +168,27 @@ func (c *Config) Validate() error {
return nil
}

func (cfg *Config) GetLogger() (*zap.Logger, error) {
loggerConfig := zap.NewProductionConfig()
loggerConfig.DisableCaller = true
if cfg.Debug {
loggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
}
if cfg.Log.Colors && cfg.Log.Format != "json" {
loggerConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
loggerConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
loggerConfig.Encoding = cfg.Log.Format
loggerConfig.OutputPaths = cfg.Log.Outputs
return loggerConfig.Build()
}

type Log struct {
Format string
Colors bool
Outputs []string
}

type CORS struct {
Enabled bool
Credentials bool
Expand Down
Loading