mirror of
https://github.com/hacdias/webdav.git
synced 2026-09-23 03:31:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6a0707fe6 | ||
|
|
947b163ea7 | ||
|
|
732cf5eff5 | ||
|
|
1e87b21bb1 | ||
|
|
6166061f20 | ||
|
|
4f8eab48ab | ||
|
|
7542860a47 | ||
|
|
3688420246 | ||
|
|
47e3f6de6f | ||
|
|
356edb8b93 | ||
|
|
b16c041d0c | ||
|
|
dc45f32af8 |
@@ -64,6 +64,7 @@ jobs:
|
|||||||
sbom: true
|
sbom: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: ${{ steps.meta.outputs.args }}
|
build-args: |
|
||||||
|
VERSION=${{ steps.meta.outputs.version }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
FROM golang:1.22-alpine3.20 AS build
|
FROM golang:1.22-alpine3.20 AS build
|
||||||
|
|
||||||
ARG DOCKER_META_VERSION="untracked"
|
ARG VERSION="untracked"
|
||||||
|
|
||||||
RUN apk --update add ca-certificates
|
RUN apk --update add ca-certificates
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ COPY ./go.sum ./
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . /webdav/
|
COPY . /webdav/
|
||||||
RUN go build -o main -ldflags="-X 'github.com/hacdias/webdav/v4/cmd.version=$DOCKER_META_VERSION'" .
|
RUN go build -o main -ldflags="-X 'github.com/hacdias/webdav/v4/cmd.version=$VERSION'" .
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|
||||||
|
|||||||
@@ -8,28 +8,110 @@ A simple and standalone [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Please refer to the [Releases page](https://github.com/hacdias/webdav/releases) for more information. There, you can either download the binaries or find the Docker commands to install WebDAV.
|
For a manual install, please refer to the [releases](https://github.com/hacdias/webdav/releases) page and download the correct binary for your system. Alternatively, you can build or install it from source using the Go toolchain. You can either clone the repository and execute `go build`, or directly install it, using:
|
||||||
|
|
||||||
|
```
|
||||||
|
go install github.com/hacdias/webdav/v4@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Docker images are provided on both [GitHub's registry](https://github.com/hacdias/webdav/pkgs/container/webdav) and [Docker Hub](https://hub.docker.com/r/hacdias/webdav). You can pull the images using one of the following two commands. Note that this commands pull the latest released version. You can use specific tags to pin specific versions, or use `main` for the development branch.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GitHub Registry
|
||||||
|
docker pull ghcr.io/hacdias/webdav:latest
|
||||||
|
|
||||||
|
# Docker Hub
|
||||||
|
docker pull hacdias/webdav:latest
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
`webdav` command line interface is really easy to use so you can easily create a WebDAV server for your own user. By default, it runs on a random free port and supports JSON, YAML and TOML configuration. An example of a YAML configuration with the default configurations:
|
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`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
port: 6060
|
||||||
|
scope: /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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run \
|
||||||
|
-p 6060:6060 \
|
||||||
|
-v $(pwd)/config.yml:/config.yml:ro \
|
||||||
|
-v $(pwd)/data:/data \
|
||||||
|
ghcr.io/hacdias/webdav -c /config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The configuration can be provided as a YAML, JSON or TOML file. Below is an example of a YAML configuration file with all the options available, as well as what they mean.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Server related settings
|
|
||||||
address: 0.0.0.0
|
address: 0.0.0.0
|
||||||
port: 0
|
port: 0
|
||||||
auth: true
|
|
||||||
|
# TLS-related settings if you want to enable TLS directly.
|
||||||
tls: false
|
tls: false
|
||||||
cert: cert.pem
|
cert: cert.pem
|
||||||
key: key.pem
|
key: key.pem
|
||||||
|
|
||||||
|
# Prefix to apply to the WebDAV path-ing. Default is "/".
|
||||||
prefix: /
|
prefix: /
|
||||||
|
|
||||||
|
# Enable or disable debug logging. Default is false.
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
# Default user settings (will be merged)
|
# Whether or not to have authentication. With authentication on, you need to
|
||||||
scope: .
|
# 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: /
|
||||||
|
|
||||||
|
# Whether the users can, by default, modify the contents. Default is false.
|
||||||
modify: true
|
modify: true
|
||||||
|
|
||||||
|
# Default permissions rules to apply at the paths.
|
||||||
rules: []
|
rules: []
|
||||||
|
|
||||||
|
# The list of users. Must be defined if auth is set to true.
|
||||||
|
users:
|
||||||
|
# Example 'admin' user with plaintext password.
|
||||||
|
- username: admin
|
||||||
|
password: admin
|
||||||
|
# Example 'john' user with bcrypt encrypted password, with custom scope.
|
||||||
|
- username: john
|
||||||
|
password: "{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi"
|
||||||
|
scope: /another/path
|
||||||
|
# Example user whose details will be picked up from the environment.
|
||||||
|
- username: "{env}ENV_USERNAME"
|
||||||
|
password: "{env}ENV_PASSWORD"
|
||||||
|
- username: basic
|
||||||
|
password: basic
|
||||||
|
# Override default modify.
|
||||||
|
modify: false
|
||||||
|
rules:
|
||||||
|
# With this rule, the user CANNOT access /some/files.
|
||||||
|
- path: /some/file
|
||||||
|
allow: false
|
||||||
|
# With this rule, the user CAN modify /public/access.
|
||||||
|
- path: /public/access/
|
||||||
|
modify: true
|
||||||
|
# With this rule, the user CAN modify all files ending with .js. It uses
|
||||||
|
# a regular expression.
|
||||||
|
- path: "^*.js$"
|
||||||
|
regex: true
|
||||||
|
modify: true
|
||||||
|
|
||||||
# CORS configuration
|
# CORS configuration
|
||||||
cors:
|
cors:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -43,32 +125,8 @@ cors:
|
|||||||
exposed_headers:
|
exposed_headers:
|
||||||
- Content-Length
|
- Content-Length
|
||||||
- Content-Range
|
- Content-Range
|
||||||
|
|
||||||
users:
|
|
||||||
- username: admin
|
|
||||||
password: admin
|
|
||||||
scope: /a/different/path
|
|
||||||
- username: encrypted
|
|
||||||
password: "{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi"
|
|
||||||
- username: "{env}ENV_USERNAME"
|
|
||||||
password: "{env}ENV_PASSWORD"
|
|
||||||
- username: basic
|
|
||||||
password: basic
|
|
||||||
modify: false
|
|
||||||
rules:
|
|
||||||
- regex: false
|
|
||||||
allow: false
|
|
||||||
path: /some/file
|
|
||||||
- path: /public/access/
|
|
||||||
modify: true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
There are more ways to customize how you run WebDAV through flags and environment variables. Please run `webdav --help` for more information on that.
|
|
||||||
|
|
||||||
### Systemd
|
|
||||||
|
|
||||||
An example of how to use this with `systemd` is on [webdav.service.example](/webdav.service.example).
|
|
||||||
|
|
||||||
### CORS
|
### CORS
|
||||||
|
|
||||||
The `allowed_*` properties are optional, the default value for each of them will be `*`. `exposed_headers` is optional as well, but is not set if not defined. Setting `credentials` to `true` will allow you to:
|
The `allowed_*` properties are optional, the default value for each of them will be `*`. `exposed_headers` is optional as well, but is not set if not defined. Setting `credentials` to `true` will allow you to:
|
||||||
@@ -76,8 +134,11 @@ The `allowed_*` properties are optional, the default value for each of them will
|
|||||||
1. Use `withCredentials = true` in javascript.
|
1. Use `withCredentials = true` in javascript.
|
||||||
2. Use the `username:password@host` syntax.
|
2. Use the `username:password@host` syntax.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
### Reverse Proxy Service
|
### Reverse Proxy Service
|
||||||
When you use a reverse proxy implementation like `Nginx` or `Apache`, please note the following fields to avoid causing `502` errors
|
|
||||||
|
When using a reverse proxy implementation, like Caddy, Nginx, or Apache, note that you need to forward the correct headers in order to avoid 502 errors. Here's a Nginx configuration example:
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
location / {
|
location / {
|
||||||
@@ -90,6 +151,27 @@ location / {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Systemd
|
||||||
|
|
||||||
|
Example configuration of a [`systemd`](https://en.wikipedia.org/wiki/Systemd) service:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
[Unit]
|
||||||
|
Description=WebDAV
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
ExecStart=/usr/bin/webdav --config /opt/webdav.yml
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Feel free to open an issue or a pull request.
|
Feel free to open an issue or a pull request.
|
||||||
|
|||||||
+8
-8
@@ -19,14 +19,14 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
flags := rootCmd.Flags()
|
flags := rootCmd.Flags()
|
||||||
flags.StringP("config", "c", "", "config file path")
|
flags.StringP("config", "c", "", "config file path")
|
||||||
flags.BoolP("tls", "t", false, "enable TLS")
|
flags.BoolP("tls", "t", lib.DefaultTLS, "enable TLS")
|
||||||
flags.Bool("auth", false, "enable authentication")
|
flags.Bool("auth", lib.DefaultAuth, "enable authentication")
|
||||||
flags.String("cert", "cert.pem", "path to TLS certificate")
|
flags.String("cert", lib.DefaultCert, "path to TLS certificate")
|
||||||
flags.String("key", "key.pem", "path to TLS key")
|
flags.String("key", lib.DefaultKey, "path to TLS key")
|
||||||
flags.StringP("address", "a", "0.0.0.0", "address to listen on")
|
flags.StringP("address", "a", lib.DefaultAddress, "address to listen on")
|
||||||
flags.StringP("port", "p", "0", "port to listen on")
|
flags.IntP("port", "p", lib.DefaultPort, "port to listen on")
|
||||||
flags.StringP("prefix", "P", "/", "URL path prefix")
|
flags.StringP("prefix", "P", lib.DefaultPrefix, "URL path prefix")
|
||||||
flags.String("log_format", "console", "logging format")
|
flags.String("log_format", lib.DefaultLogFormat, "logging format")
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
|
|||||||
+35
-9
@@ -10,6 +10,17 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultTLS = false
|
||||||
|
DefaultAuth = false
|
||||||
|
DefaultCert = "cert.pem"
|
||||||
|
DefaultKey = "key.pem"
|
||||||
|
DefaultAddress = "0.0.0.0"
|
||||||
|
DefaultPort = 0
|
||||||
|
DefaultPrefix = "/"
|
||||||
|
DefaultLogFormat = "console"
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Permissions `mapstructure:",squash"`
|
Permissions `mapstructure:",squash"`
|
||||||
Debug bool
|
Debug bool
|
||||||
@@ -20,7 +31,7 @@ type Config struct {
|
|||||||
Key string
|
Key string
|
||||||
Prefix string
|
Prefix string
|
||||||
NoSniff bool
|
NoSniff bool
|
||||||
LogFormat string
|
LogFormat string `mapstructure:"log_format"`
|
||||||
Auth bool
|
Auth bool
|
||||||
CORS CORS
|
CORS CORS
|
||||||
Users []User
|
Users []User
|
||||||
@@ -55,10 +66,20 @@ func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
|
|||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
v.AutomaticEnv()
|
v.AutomaticEnv()
|
||||||
|
|
||||||
// Defaults
|
// Defaults shared with flags
|
||||||
v.SetDefault("CORS.AllowedHeaders", []string{"*"})
|
v.SetDefault("TLS", DefaultTLS)
|
||||||
v.SetDefault("CORS.AllowedHosts", []string{"*"})
|
v.SetDefault("Cert", DefaultCert)
|
||||||
v.SetDefault("CORS.AllowedMethods", []string{"*"})
|
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("CORS.Allowed_Headers", []string{"*"})
|
||||||
|
v.SetDefault("CORS.Allowed_Hosts", []string{"*"})
|
||||||
|
v.SetDefault("CORS.Allowed_Methods", []string{"*"})
|
||||||
|
|
||||||
// Read and unmarshal configuration
|
// Read and unmarshal configuration
|
||||||
err := v.ReadInConfig()
|
err := v.ReadInConfig()
|
||||||
@@ -108,6 +129,11 @@ func (c *Config) Validate() error {
|
|||||||
return errors.New("invalid config: auth cannot be disabled with users defined")
|
return errors.New("invalid config: auth cannot be disabled with users defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Scope, err = filepath.Abs(c.Scope)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if c.TLS {
|
if c.TLS {
|
||||||
if c.Cert == "" {
|
if c.Cert == "" {
|
||||||
return errors.New("invalid config: Cert must be defined if TLS is activated")
|
return errors.New("invalid config: Cert must be defined if TLS is activated")
|
||||||
@@ -146,8 +172,8 @@ func (c *Config) Validate() error {
|
|||||||
type CORS struct {
|
type CORS struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Credentials bool
|
Credentials bool
|
||||||
AllowedHeaders []string
|
AllowedHeaders []string `mapstructure:"allowed_headers"`
|
||||||
AllowedHosts []string
|
AllowedHosts []string `mapstructure:"allowed_hosts"`
|
||||||
AllowedMethods []string
|
AllowedMethods []string `mapstructure:"allowed_methods"`
|
||||||
ExposedHeaders []string
|
ExposedHeaders []string `mapstructure:"exposed_headers"`
|
||||||
}
|
}
|
||||||
|
|||||||
+141
-15
@@ -8,9 +8,9 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeAndParseConfig(t *testing.T, content string) *Config {
|
func writeAndParseConfig(t *testing.T, content, extension string) *Config {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
tmpFile := filepath.Join(tmpDir, "config.yml")
|
tmpFile := filepath.Join(tmpDir, "config"+extension)
|
||||||
|
|
||||||
err := os.WriteFile(tmpFile, []byte(content), 0666)
|
err := os.WriteFile(tmpFile, []byte(content), 0666)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -24,9 +24,17 @@ func writeAndParseConfig(t *testing.T, content string) *Config {
|
|||||||
func TestConfigDefaults(t *testing.T) {
|
func TestConfigDefaults(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := writeAndParseConfig(t, "")
|
cfg := writeAndParseConfig(t, "", ".yml")
|
||||||
require.NoError(t, cfg.Validate())
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
require.EqualValues(t, DefaultAuth, cfg.Auth)
|
||||||
|
require.EqualValues(t, DefaultTLS, cfg.TLS)
|
||||||
|
require.EqualValues(t, DefaultAddress, cfg.Address)
|
||||||
|
require.EqualValues(t, DefaultPort, cfg.Port)
|
||||||
|
require.EqualValues(t, DefaultPrefix, cfg.Prefix)
|
||||||
|
require.EqualValues(t, DefaultLogFormat, cfg.LogFormat)
|
||||||
|
require.NotEmpty(t, cfg.Scope)
|
||||||
|
|
||||||
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedHeaders)
|
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedHeaders)
|
||||||
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedHosts)
|
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedHosts)
|
||||||
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedMethods)
|
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedMethods)
|
||||||
@@ -35,7 +43,24 @@ func TestConfigDefaults(t *testing.T) {
|
|||||||
func TestConfigCascade(t *testing.T) {
|
func TestConfigCascade(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
content := `
|
check := func(t *testing.T, cfg *Config) {
|
||||||
|
require.True(t, cfg.Modify)
|
||||||
|
require.Equal(t, "/", cfg.Scope)
|
||||||
|
require.Len(t, cfg.Rules, 1)
|
||||||
|
|
||||||
|
require.Len(t, cfg.Users, 2)
|
||||||
|
|
||||||
|
require.True(t, cfg.Users[0].Modify)
|
||||||
|
require.Equal(t, "/", cfg.Users[0].Scope)
|
||||||
|
require.Len(t, cfg.Users[0].Rules, 1)
|
||||||
|
|
||||||
|
require.False(t, cfg.Users[1].Modify)
|
||||||
|
require.Equal(t, "/basic", cfg.Users[1].Scope)
|
||||||
|
require.Len(t, cfg.Users[1].Rules, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("YAML", func(t *testing.T) {
|
||||||
|
content := `
|
||||||
auth: true
|
auth: true
|
||||||
scope: /
|
scope: /
|
||||||
modify: true
|
modify: true
|
||||||
@@ -52,20 +77,121 @@ users:
|
|||||||
modify: false
|
modify: false
|
||||||
rules: []`
|
rules: []`
|
||||||
|
|
||||||
cfg := writeAndParseConfig(t, content)
|
cfg := writeAndParseConfig(t, content, ".yml")
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
check(t, cfg)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON", func(t *testing.T) {
|
||||||
|
content := `{
|
||||||
|
"auth": true,
|
||||||
|
"scope": "/",
|
||||||
|
"modify": true,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"path": "/public/access/",
|
||||||
|
"modify": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "basic",
|
||||||
|
"password": "basic",
|
||||||
|
"scope": "/basic",
|
||||||
|
"modify": false,
|
||||||
|
"rules": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
cfg := writeAndParseConfig(t, content, ".json")
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
check(t, cfg)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("`TOML", func(t *testing.T) {
|
||||||
|
content := `auth = true
|
||||||
|
scope = "/"
|
||||||
|
modify = true
|
||||||
|
|
||||||
|
[[rules]]
|
||||||
|
path = "/public/access/"
|
||||||
|
modify = true
|
||||||
|
|
||||||
|
[[users]]
|
||||||
|
username = "admin"
|
||||||
|
password = "admin"
|
||||||
|
|
||||||
|
[[users]]
|
||||||
|
username = "basic"
|
||||||
|
password = "basic"
|
||||||
|
scope = "/basic"
|
||||||
|
modify = false
|
||||||
|
rules = []
|
||||||
|
`
|
||||||
|
|
||||||
|
cfg := writeAndParseConfig(t, content, ".toml")
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
check(t, cfg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigKeys(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cfg := writeAndParseConfig(t, `
|
||||||
|
cors:
|
||||||
|
enabled: true
|
||||||
|
credentials: true
|
||||||
|
allowed_headers:
|
||||||
|
- Depth
|
||||||
|
allowed_hosts:
|
||||||
|
- http://localhost:8080
|
||||||
|
allowed_methods:
|
||||||
|
- GET
|
||||||
|
exposed_headers:
|
||||||
|
- Content-Length
|
||||||
|
- Content-Range`, ".yml")
|
||||||
require.NoError(t, cfg.Validate())
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
require.True(t, cfg.Modify)
|
require.True(t, cfg.CORS.Enabled)
|
||||||
require.Equal(t, "/", cfg.Scope)
|
require.True(t, cfg.CORS.Credentials)
|
||||||
require.Len(t, cfg.Rules, 1)
|
require.EqualValues(t, []string{"Content-Length", "Content-Range"}, cfg.CORS.ExposedHeaders)
|
||||||
|
require.EqualValues(t, []string{"Depth"}, cfg.CORS.AllowedHeaders)
|
||||||
|
require.EqualValues(t, []string{"http://localhost:8080"}, cfg.CORS.AllowedHosts)
|
||||||
|
require.EqualValues(t, []string{"GET"}, cfg.CORS.AllowedMethods)
|
||||||
|
}
|
||||||
|
|
||||||
require.Len(t, cfg.Users, 2)
|
func TestConfigRules(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
auth: false
|
||||||
|
scope: /
|
||||||
|
modify: true
|
||||||
|
rules:
|
||||||
|
- path: '^.+\.js$'
|
||||||
|
regex: true
|
||||||
|
modify: true
|
||||||
|
- path: /public/access/
|
||||||
|
regex: false
|
||||||
|
modify: true`
|
||||||
|
|
||||||
require.True(t, cfg.Users[0].Modify)
|
cfg := writeAndParseConfig(t, content, ".yaml")
|
||||||
require.Equal(t, "/", cfg.Users[0].Scope)
|
require.NoError(t, cfg.Validate())
|
||||||
require.Len(t, cfg.Users[0].Rules, 1)
|
|
||||||
|
|
||||||
require.False(t, cfg.Users[1].Modify)
|
require.Len(t, cfg.Rules, 2)
|
||||||
require.Equal(t, "/basic", cfg.Users[1].Scope)
|
|
||||||
require.Len(t, cfg.Users[1].Rules, 0)
|
require.Empty(t, cfg.Rules[0].Path)
|
||||||
|
require.NotNil(t, cfg.Rules[0].Regexp)
|
||||||
|
require.True(t, cfg.Rules[0].Regexp.MatchString("/my/path/to/file.js"))
|
||||||
|
require.False(t, cfg.Rules[0].Regexp.MatchString("/my/path/to/file.ts"))
|
||||||
|
|
||||||
|
require.NotEmpty(t, cfg.Rules[1].Path)
|
||||||
|
require.Nil(t, cfg.Rules[1].Regexp)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -15,7 +15,6 @@ type handlerUser struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*Config
|
|
||||||
user *handlerUser
|
user *handlerUser
|
||||||
users map[string]*handlerUser
|
users map[string]*handlerUser
|
||||||
}
|
}
|
||||||
@@ -70,7 +69,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
user := h.user
|
user := h.user
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
if h.Auth {
|
if len(h.users) > 0 {
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||||
|
|
||||||
// Gets the correct user for this request.
|
// Gets the correct user for this request.
|
||||||
@@ -107,7 +106,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == "HEAD" {
|
if r.Method == "HEAD" {
|
||||||
w = newResponseWriterNoBody(w)
|
w = responseWriterNoBody{w}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Excerpt from RFC4918, section 9.4:
|
// Excerpt from RFC4918, section 9.4:
|
||||||
@@ -131,3 +130,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Runs the WebDAV.
|
// Runs the WebDAV.
|
||||||
user.ServeHTTP(w, r)
|
user.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseWriterNoBody struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w responseWriterNoBody) Write(data []byte) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -31,6 +31,7 @@ func (r *Rule) Validate() error {
|
|||||||
}
|
}
|
||||||
r.Regexp = rp
|
r.Regexp = rp
|
||||||
r.Path = ""
|
r.Path = ""
|
||||||
|
r.Regex = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -38,7 +39,7 @@ func (r *Rule) Validate() error {
|
|||||||
|
|
||||||
// Matches checks if [Rule] matches the given path.
|
// Matches checks if [Rule] matches the given path.
|
||||||
func (r *Rule) Matches(path string) bool {
|
func (r *Rule) Matches(path string) bool {
|
||||||
if r.Regex {
|
if r.Regexp != nil {
|
||||||
return r.Regexp.MatchString(path)
|
return r.Regexp.MatchString(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
var _ http.ResponseWriter = responseWriterNoBody{}
|
|
||||||
|
|
||||||
// responseWriterNoBody is a wrapper used to suppress the body of the response
|
|
||||||
// to a request. Mainly used for HEAD requests.
|
|
||||||
type responseWriterNoBody struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
// newResponseWriterNoBody creates a new responseWriterNoBody.
|
|
||||||
func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
|
|
||||||
return &responseWriterNoBody{w}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write suppress the body.
|
|
||||||
func (w responseWriterNoBody) Write(data []byte) (int, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteHeader writes the header to the http.ResponseWriter.
|
|
||||||
func (w responseWriterNoBody) WriteHeader(statusCode int) {
|
|
||||||
w.Header().Del("Content-Length")
|
|
||||||
w.ResponseWriter.WriteHeader(statusCode)
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=WebDAV server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
ExecStart=/usr/bin/webdav --config /opt/webdav.config
|
|
||||||
Restart=on-failure
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
Reference in New Issue
Block a user