mirror of
https://github.com/hacdias/webdav.git
synced 2026-09-22 03:20:41 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1691e1bd1 | ||
|
|
5f685dbe98 | ||
|
|
85219df921 | ||
|
|
c125bedae1 | ||
|
|
46d54e4465 | ||
|
|
90c031846d | ||
|
|
968f2e147a | ||
|
|
6f4be12e8c | ||
|
|
9433dbd452 | ||
|
|
f0ca85e570 | ||
|
|
436a3b05a1 | ||
|
|
1a610b17ba | ||
|
|
32613f76cc | ||
|
|
8a8650d9b2 | ||
|
|
5701cbb5b8 | ||
|
|
9a4b378b32 | ||
|
|
099479a894 | ||
|
|
8cd6d0a585 | ||
|
|
fca4e54839 | ||
|
|
da6dd253d5 | ||
|
|
c1a919681b | ||
|
|
5734366c54 | ||
|
|
aab63c0ccc | ||
|
|
f212531d75 | ||
|
|
2a39779cff | ||
|
|
34f2336d91 | ||
|
|
6c5420aa5a | ||
|
|
30c210d097 | ||
|
|
f408f7aa6f | ||
|
|
e9cd29578e | ||
|
|
e0020e8110 | ||
|
|
2078312ec8 | ||
|
|
05e995b11b | ||
|
|
c4d4734095 | ||
|
|
cf267c1006 | ||
|
|
72d8e39927 | ||
|
|
3ef86e8a7f | ||
|
|
2971f7ed3d | ||
|
|
ab0334f036 | ||
|
|
931f125224 | ||
|
|
cd472b26be | ||
|
|
7358553e69 | ||
|
|
764a69cd33 | ||
|
|
d266f1150e | ||
|
|
76ebaffaef | ||
|
|
60f2697615 | ||
|
|
e5b3946388 | ||
|
|
8c66f0c585 | ||
|
|
bd7e2d8158 | ||
|
|
552c72987d | ||
|
|
e022bad4bd | ||
|
|
82b3da92bd | ||
|
|
0b1da0c3c4 | ||
|
|
62ca86bf4e | ||
|
|
5afeb0eb60 | ||
|
|
4bb814bed6 | ||
|
|
c5f3907994 | ||
|
|
e81638120b | ||
|
|
f172f1bca1 | ||
|
|
24fa8aa228 | ||
|
|
78ceb0e44c | ||
|
|
5239649127 | ||
|
|
35fd913321 | ||
|
|
dd4a57af2e | ||
|
|
a037fe877c | ||
|
|
81f873d2de | ||
|
|
505a10a63d | ||
|
|
61462c1da7 | ||
|
|
21ec95a8db | ||
|
|
8bf2c4eea3 | ||
|
|
4199ffc190 | ||
|
|
7a67f76947 | ||
|
|
088958913c | ||
|
|
3e7b39679b | ||
|
|
d95c1ec13b | ||
|
|
3d06904d1f | ||
|
|
4dc7bae6ea | ||
|
|
6b139d375c | ||
|
|
80e3ffd57b | ||
|
|
893d3b429a | ||
|
|
cdaf929a4b | ||
|
|
d216a2edef | ||
|
|
eeae21cd29 |
@@ -0,0 +1,19 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.x"
|
||||
- run: go build .
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
hacdias/webdav
|
||||
ghcr.io/hacdias/webdav
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern=v{{version}}
|
||||
type=semver,pattern=v{{major}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
sbom: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: ${{ steps.meta.outputs.args }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.x"
|
||||
- uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: "v1.59"
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Releaser
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.x"
|
||||
- name: Run test with coverage
|
||||
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
+2
-3
@@ -1,3 +1,2 @@
|
||||
config.yaml
|
||||
config.yml
|
||||
config.json
|
||||
dist/
|
||||
webdav
|
||||
+54
-28
@@ -1,29 +1,55 @@
|
||||
build:
|
||||
main: cmd/webdav/main.go
|
||||
binary: webdav
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- netbsd
|
||||
- openbsd
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
version: 2
|
||||
|
||||
archive:
|
||||
name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}"
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- main: main.go
|
||||
binary: webdav
|
||||
flags:
|
||||
- '-trimpath'
|
||||
ldflags:
|
||||
- '-X github.com/hacdias/webdav/v4/cmd.version={{.Version}}'
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- netbsd
|
||||
- openbsd
|
||||
goarch:
|
||||
- amd64
|
||||
- '386'
|
||||
- arm
|
||||
- arm64
|
||||
- mips
|
||||
- mipsle
|
||||
- mips64
|
||||
- mips64le
|
||||
goarm:
|
||||
- '5'
|
||||
- '6'
|
||||
- '7'
|
||||
ignore:
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
|
||||
archives:
|
||||
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: hacdias
|
||||
name: webdav
|
||||
draft: false
|
||||
prerelease: auto
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
language: go
|
||||
|
||||
go: 1.8.3
|
||||
|
||||
env:
|
||||
- "PATH=/home/travis/gopath/bin:$PATH"
|
||||
|
||||
install:
|
||||
- go get ./...
|
||||
# Install gometalinter and certain linters
|
||||
- go get github.com/alecthomas/gometalinter
|
||||
- gometalinter --install
|
||||
|
||||
script:
|
||||
- gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --tests ./...
|
||||
- go test ./... -timeout 30s
|
||||
|
||||
after_success:
|
||||
- test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
FROM golang:1.22-alpine3.20 AS build
|
||||
|
||||
ARG DOCKER_META_VERSION="untracked"
|
||||
|
||||
RUN apk --update add ca-certificates
|
||||
|
||||
WORKDIR /webdav/
|
||||
|
||||
COPY ./go.mod ./
|
||||
COPY ./go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . /webdav/
|
||||
RUN go build -o main -ldflags="-X 'github.com/hacdias/webdav/v4/cmd.version=$DOCKER_META_VERSION'" .
|
||||
|
||||
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
|
||||
|
||||
ENTRYPOINT [ "webdav" ]
|
||||
CMD [ "-p", "80" ]
|
||||
@@ -0,0 +1,99 @@
|
||||
# webdav
|
||||
|
||||
[](https://goreportcard.com/report/hacdias/webdav)
|
||||
[](https://github.com/hacdias/webdav/releases/latest)
|
||||
[](https://hub.docker.com/r/hacdias/webdav)
|
||||
|
||||
A simple and standalone [WebDAV](https://en.wikipedia.org/wiki/WebDAV) server.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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:
|
||||
|
||||
```yaml
|
||||
# Server related settings
|
||||
address: 0.0.0.0
|
||||
port: 0
|
||||
auth: true
|
||||
tls: false
|
||||
cert: cert.pem
|
||||
key: key.pem
|
||||
prefix: /
|
||||
debug: false
|
||||
|
||||
# Default user settings (will be merged)
|
||||
scope: .
|
||||
modify: true
|
||||
rules: []
|
||||
|
||||
# CORS configuration
|
||||
cors:
|
||||
enabled: true
|
||||
credentials: true
|
||||
allowed_headers:
|
||||
- Depth
|
||||
allowed_hosts:
|
||||
- http://localhost:8080
|
||||
allowed_methods:
|
||||
- GET
|
||||
exposed_headers:
|
||||
- Content-Length
|
||||
- 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
|
||||
|
||||
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:
|
||||
|
||||
1. Use `withCredentials = true` in javascript.
|
||||
2. Use the `username:password@host` syntax.
|
||||
|
||||
### Reverse Proxy Service
|
||||
When you use a reverse proxy implementation like `Nginx` or `Apache`, please note the following fields to avoid causing `502` errors
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_redirect off;
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to open an issue or a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](LICENSE) © [Henrique Dias](https://hacdias.com)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to:
|
||||
msaa1990 [at] gmail [dot com]
|
||||
|
||||
cc: hacdias [at] gmail [dot com]
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// Execute executes the commands.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"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.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")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "webdav",
|
||||
Short: "A simple to use WebDAV server",
|
||||
Long: `If you don't set "config", it will look for a configuration file called
|
||||
config.{json, toml, yaml, yml} in the following directories:
|
||||
|
||||
- ./
|
||||
- /etc/webdav/
|
||||
|
||||
The precedence of the configuration values are as follows:
|
||||
|
||||
- flags
|
||||
- environment variables
|
||||
- configuration file
|
||||
- defaults
|
||||
|
||||
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.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
flags := cmd.Flags()
|
||||
|
||||
cfgFilename, _ := flags.GetString("config")
|
||||
|
||||
cfg, err := lib.ParseConfig(cfgFilename, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create HTTP handler from the config
|
||||
handler, err := lib.NewHandler(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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()
|
||||
}()
|
||||
|
||||
// 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 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 {
|
||||
address = fmt.Sprintf("%s:%d", cfg.Address, cfg.Port)
|
||||
network = "tcp"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var version = "untracked"
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("WebDAV version: %s\n", version)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hacdias/webdav"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
wd "golang.org/x/net/webdav"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
config string
|
||||
defaultConfigs = []string{
|
||||
"config.json",
|
||||
"config.yaml",
|
||||
"config.yml",
|
||||
"/etc/webdav/config.json",
|
||||
"/etc/webdav/config.yaml",
|
||||
"/etc/webdav/config.yml",
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&config, "config", "", "Configuration file")
|
||||
}
|
||||
|
||||
func parseRules(raw []map[string]interface{}) []*webdav.Rule {
|
||||
rules := []*webdav.Rule{}
|
||||
|
||||
for _, r := range raw {
|
||||
rule := &webdav.Rule{
|
||||
Regex: false,
|
||||
Allow: false,
|
||||
Path: "",
|
||||
}
|
||||
|
||||
if regex, ok := r["regex"].(bool); ok {
|
||||
rule.Regex = regex
|
||||
}
|
||||
|
||||
if allow, ok := r["allow"].(bool); ok {
|
||||
rule.Allow = allow
|
||||
}
|
||||
|
||||
path, ok := r["rule"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if rule.Regex {
|
||||
rule.Regexp = regexp.MustCompile(path)
|
||||
} else {
|
||||
rule.Path = path
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
func parseUsers(raw []map[string]interface{}, c *cfg) {
|
||||
for _, r := range raw {
|
||||
username, ok := r["username"].(string)
|
||||
if !ok {
|
||||
log.Fatal("user needs an username")
|
||||
}
|
||||
|
||||
password, ok := r["password"].(string)
|
||||
if !ok {
|
||||
log.Fatal("user needs a password")
|
||||
}
|
||||
|
||||
c.auth[username] = password
|
||||
|
||||
user := &webdav.User{
|
||||
Scope: c.webdav.User.Scope,
|
||||
Modify: c.webdav.User.Modify,
|
||||
Rules: c.webdav.User.Rules,
|
||||
}
|
||||
|
||||
if scope, ok := r["scope"].(string); ok {
|
||||
user.Scope = scope
|
||||
}
|
||||
|
||||
if modify, ok := r["modify"].(bool); ok {
|
||||
user.Modify = modify
|
||||
}
|
||||
|
||||
if rules, ok := r["rules"].([]map[string]interface{}); ok {
|
||||
user.Rules = parseRules(rules)
|
||||
}
|
||||
|
||||
user.Handler = &wd.Handler{
|
||||
FileSystem: wd.Dir(user.Scope),
|
||||
LockSystem: wd.NewMemLS(),
|
||||
}
|
||||
|
||||
c.webdav.Users[username] = user
|
||||
}
|
||||
}
|
||||
|
||||
func getConfig() []byte {
|
||||
if config == "" {
|
||||
for _, v := range defaultConfigs {
|
||||
_, err := os.Stat(v)
|
||||
if err == nil {
|
||||
config = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config == "" {
|
||||
log.Fatal("no config file specified; couldn't find any config.{yaml,json}")
|
||||
}
|
||||
|
||||
file, err := ioutil.ReadFile(config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
type cfg struct {
|
||||
webdav *webdav.Config
|
||||
address string
|
||||
port string
|
||||
auth map[string]string
|
||||
}
|
||||
|
||||
func parseConfig() *cfg {
|
||||
file := getConfig()
|
||||
|
||||
data := struct {
|
||||
Address string `json:"address" yaml:"address"`
|
||||
Port string `json:"port" yaml:"port"`
|
||||
Scope string `json:"scope" yaml:"scope"`
|
||||
Modify bool `json:"modify" yaml:"modify"`
|
||||
Rules []map[string]interface{} `json:"rules" yaml:"rules"`
|
||||
Users []map[string]interface{} `json:"users" yaml:"users"`
|
||||
}{
|
||||
Address: "0.0.0.0",
|
||||
Port: "0",
|
||||
Scope: "./",
|
||||
Modify: true,
|
||||
}
|
||||
|
||||
var err error
|
||||
if filepath.Ext(config) == ".json" {
|
||||
err = json.Unmarshal(file, &data)
|
||||
} else {
|
||||
err = yaml.Unmarshal(file, &data)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
config := &cfg{
|
||||
address: data.Address,
|
||||
port: data.Port,
|
||||
auth: map[string]string{},
|
||||
webdav: &webdav.Config{
|
||||
User: &webdav.User{
|
||||
Scope: data.Scope,
|
||||
Modify: data.Modify,
|
||||
Rules: []*webdav.Rule{},
|
||||
Handler: &wd.Handler{
|
||||
FileSystem: wd.Dir(data.Scope),
|
||||
LockSystem: wd.NewMemLS(),
|
||||
},
|
||||
},
|
||||
Users: map[string]*webdav.User{},
|
||||
},
|
||||
}
|
||||
|
||||
if len(data.Users) == 0 {
|
||||
log.Fatal("no user defined")
|
||||
}
|
||||
|
||||
if len(data.Rules) != 0 {
|
||||
config.webdav.User.Rules = parseRules(data.Rules)
|
||||
}
|
||||
|
||||
parseUsers(data.Users, config)
|
||||
return config
|
||||
}
|
||||
|
||||
func basicAuth(c *cfg) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
|
||||
username, password, authOK := r.BasicAuth()
|
||||
if authOK == false {
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
p, ok := c.auth[username]
|
||||
if !ok {
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
if !checkPassword(p, password) {
|
||||
log.Println("Wrong Password for user", username)
|
||||
http.Error(w, "Not authorized", 401)
|
||||
return
|
||||
}
|
||||
|
||||
c.webdav.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func checkPassword(saved, input string) bool {
|
||||
|
||||
if strings.HasPrefix(saved, "{bcrypt}") {
|
||||
savedPassword := strings.TrimPrefix(saved, "{bcrypt}")
|
||||
return bcrypt.CompareHashAndPassword([]byte(savedPassword), []byte(input)) == nil
|
||||
}
|
||||
|
||||
return saved == input
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
cfg := parseConfig()
|
||||
handler := basicAuth(cfg)
|
||||
|
||||
// Builds the address and a listener.
|
||||
laddr := cfg.address + ":" + cfg.port
|
||||
listener, err := net.Listen("tcp", laddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Tell the user the port in which is listening.
|
||||
fmt.Println("Listening on", listener.Addr().String())
|
||||
|
||||
// Starts the server.
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
module github.com/hacdias/webdav/v4
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/rs/cors v1.11.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/net v0.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
retract v4.1.0
|
||||
@@ -0,0 +1,84 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
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/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=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
|
||||
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Permissions `mapstructure:",squash"`
|
||||
Debug bool
|
||||
Address string
|
||||
Port int
|
||||
TLS bool
|
||||
Cert string
|
||||
Key string
|
||||
Prefix string
|
||||
NoSniff bool
|
||||
LogFormat string
|
||||
Auth bool
|
||||
CORS CORS
|
||||
Users []User
|
||||
}
|
||||
|
||||
func ParseConfig(filename string, flags *pflag.FlagSet) (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
// Configure flags bindings
|
||||
if flags != nil {
|
||||
err := v.BindPFlags(flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = v.BindPFlag("LogFormat", flags.Lookup("log_format"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration file settings
|
||||
v.AddConfigPath(".")
|
||||
v.AddConfigPath("/etc/webdav/")
|
||||
v.SetConfigName("config")
|
||||
if filename != "" {
|
||||
v.SetConfigFile(filename)
|
||||
}
|
||||
|
||||
// Environment settings
|
||||
v.SetEnvPrefix("wd")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
|
||||
// Defaults
|
||||
v.SetDefault("CORS.AllowedHeaders", []string{"*"})
|
||||
v.SetDefault("CORS.AllowedHosts", []string{"*"})
|
||||
v.SetDefault("CORS.AllowedMethods", []string{"*"})
|
||||
|
||||
// Read and unmarshal configuration
|
||||
err := v.ReadInConfig()
|
||||
if err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cfg := &Config{}
|
||||
err = v.Unmarshal(cfg)
|
||||
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.Modify", i)) {
|
||||
cfg.Users[i].Modify = cfg.Modify
|
||||
}
|
||||
|
||||
if !v.IsSet(fmt.Sprintf("Users.%d.Rules", i)) {
|
||||
cfg.Users[i].Rules = cfg.Rules
|
||||
}
|
||||
}
|
||||
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if c.TLS {
|
||||
if c.Cert == "" {
|
||||
return errors.New("invalid config: Cert must be defined if TLS is activated")
|
||||
}
|
||||
|
||||
if c.Key == "" {
|
||||
return errors.New("invalid config: Key must be defined if TLS is activated")
|
||||
}
|
||||
|
||||
c.Cert, err = filepath.Abs(c.Cert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
c.Key, err = filepath.Abs(c.Key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = c.Permissions.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
for _, u := range c.Users {
|
||||
err := u.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CORS struct {
|
||||
Enabled bool
|
||||
Credentials bool
|
||||
AllowedHeaders []string
|
||||
AllowedHosts []string
|
||||
AllowedMethods []string
|
||||
ExposedHeaders []string
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func writeAndParseConfig(t *testing.T, content string) *Config {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
err := os.WriteFile(tmpFile, []byte(content), 0666)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := ParseConfig(tmpFile, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func TestConfigDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := writeAndParseConfig(t, "")
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedHeaders)
|
||||
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedHosts)
|
||||
require.EqualValues(t, []string{"*"}, cfg.CORS.AllowedMethods)
|
||||
}
|
||||
|
||||
func TestConfigCascade(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mime"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
type Dir struct {
|
||||
webdav.Dir
|
||||
noSniff bool
|
||||
}
|
||||
|
||||
func (d Dir) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||
// Skip wrapping if NoSniff is off
|
||||
if !d.noSniff {
|
||||
return d.Dir.Stat(ctx, name)
|
||||
}
|
||||
|
||||
info, err := d.Dir.Stat(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return noSniffFileInfo{info}, nil
|
||||
}
|
||||
|
||||
func (d Dir) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
// Skip wrapping if NoSniff is off
|
||||
if !d.noSniff {
|
||||
return d.Dir.OpenFile(ctx, name, flag, perm)
|
||||
}
|
||||
|
||||
file, err := d.Dir.OpenFile(ctx, name, flag, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return noSniffFile{File: file}, nil
|
||||
}
|
||||
|
||||
type noSniffFileInfo struct {
|
||||
os.FileInfo
|
||||
}
|
||||
|
||||
func (w noSniffFileInfo) ContentType(ctx context.Context) (contentType string, err error) {
|
||||
if mimeType := mime.TypeByExtension(path.Ext(w.FileInfo.Name())); mimeType != "" {
|
||||
// We can figure out the mime from the extension.
|
||||
return mimeType, nil
|
||||
} else {
|
||||
// We can't figure out the mime type without sniffing, call it an octet stream.
|
||||
return "application/octet-stream", nil
|
||||
}
|
||||
}
|
||||
|
||||
type noSniffFile struct {
|
||||
webdav.File
|
||||
}
|
||||
|
||||
func (f noSniffFile) Stat() (os.FileInfo, error) {
|
||||
info, err := f.File.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return noSniffFileInfo{info}, nil
|
||||
}
|
||||
|
||||
func (f noSniffFile) Readdir(count int) (fis []os.FileInfo, err error) {
|
||||
fis, err = f.File.Readdir(count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range fis {
|
||||
fis[i] = noSniffFileInfo{fis[i]}
|
||||
}
|
||||
return fis, nil
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/cors"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
type handlerUser struct {
|
||||
User
|
||||
webdav.Handler
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
*Config
|
||||
user *handlerUser
|
||||
users map[string]*handlerUser
|
||||
}
|
||||
|
||||
func NewHandler(c *Config) (http.Handler, error) {
|
||||
h := &Handler{
|
||||
user: &handlerUser{
|
||||
User: User{
|
||||
Permissions: c.Permissions,
|
||||
},
|
||||
Handler: webdav.Handler{
|
||||
Prefix: c.Prefix,
|
||||
FileSystem: Dir{
|
||||
Dir: webdav.Dir(c.Scope),
|
||||
noSniff: c.NoSniff,
|
||||
},
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
},
|
||||
},
|
||||
users: map[string]*handlerUser{},
|
||||
}
|
||||
|
||||
for _, u := range c.Users {
|
||||
h.users[u.Username] = &handlerUser{
|
||||
User: u,
|
||||
Handler: webdav.Handler{
|
||||
Prefix: c.Prefix,
|
||||
FileSystem: Dir{
|
||||
Dir: webdav.Dir(u.Scope),
|
||||
noSniff: c.NoSniff,
|
||||
},
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if c.CORS.Enabled {
|
||||
return cors.New(cors.Options{
|
||||
AllowCredentials: c.CORS.Credentials,
|
||||
AllowedOrigins: c.CORS.AllowedHosts,
|
||||
AllowedMethods: c.CORS.AllowedMethods,
|
||||
AllowedHeaders: c.CORS.AllowedHeaders,
|
||||
OptionsPassthrough: false,
|
||||
}).Handler(h), nil
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
user := h.user
|
||||
|
||||
// Authentication
|
||||
if h.Auth {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
|
||||
// Gets the correct user for this request.
|
||||
username, password, ok := r.BasicAuth()
|
||||
zap.L().Info("login attempt", zap.String("username", username), zap.String("remote_address", r.RemoteAddr))
|
||||
if !ok {
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
user, ok = h.users[username]
|
||||
if !ok {
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !user.checkPassword(password) {
|
||||
zap.L().Info("invalid password", zap.String("username", username), zap.String("remote_address", r.RemoteAddr))
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
zap.L().Info("user authorized", zap.String("username", username))
|
||||
}
|
||||
|
||||
// Checks for user permissions relatively to this PATH.
|
||||
allowed := user.Allowed(r)
|
||||
|
||||
zap.L().Debug("allowed & method & path", zap.Bool("allowed", allowed), zap.String("method", r.Method), zap.String("path", r.URL.Path))
|
||||
|
||||
if !allowed {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "HEAD" {
|
||||
w = newResponseWriterNoBody(w)
|
||||
}
|
||||
|
||||
// Excerpt from RFC4918, section 9.4:
|
||||
//
|
||||
// GET, when applied to a collection, may return the contents of an
|
||||
// "index.html" resource, a human-readable view of the contents of
|
||||
// the collection, or something else altogether.
|
||||
//
|
||||
// Get, when applied to collection, will return the same as PROPFIND method.
|
||||
if r.Method == "GET" && strings.HasPrefix(r.URL.Path, user.Prefix) {
|
||||
info, err := user.FileSystem.Stat(r.Context(), strings.TrimPrefix(r.URL.Path, user.Prefix))
|
||||
if err == nil && info.IsDir() {
|
||||
r.Method = "PROPFIND"
|
||||
|
||||
if r.Header.Get("Depth") == "" {
|
||||
r.Header.Add("Depth", "1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Runs the WebDAV.
|
||||
user.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var readMethods = []string{
|
||||
http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodOptions,
|
||||
"PROPFIND",
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Regex bool
|
||||
Allow bool
|
||||
Modify bool
|
||||
Path string
|
||||
// TODO: remove Regex and replace by this. It encodes
|
||||
Regexp *regexp.Regexp `mapstructure:"-"`
|
||||
}
|
||||
|
||||
func (r *Rule) Validate() error {
|
||||
if r.Regex {
|
||||
rp, err := regexp.Compile(r.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid rule: %w", err)
|
||||
}
|
||||
r.Regexp = rp
|
||||
r.Path = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Matches checks if [Rule] matches the given path.
|
||||
func (r *Rule) Matches(path string) bool {
|
||||
if r.Regex {
|
||||
return r.Regexp.MatchString(path)
|
||||
}
|
||||
|
||||
return strings.HasPrefix(path, r.Path)
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
Scope string
|
||||
Modify bool
|
||||
Rules []*Rule
|
||||
}
|
||||
|
||||
// Allowed checks if the user has permission to access a directory/file
|
||||
func (p Permissions) Allowed(r *http.Request) bool {
|
||||
// Determine whether or not it is a read or write request.
|
||||
readRequest := false
|
||||
for _, method := range readMethods {
|
||||
if r.Method == method {
|
||||
readRequest = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Go through rules beginning from the last one.
|
||||
for i := len(p.Rules) - 1; i >= 0; i-- {
|
||||
rule := p.Rules[i]
|
||||
|
||||
if rule.Matches(r.URL.Path) {
|
||||
return rule.Allow && (readRequest || rule.Modify)
|
||||
}
|
||||
}
|
||||
|
||||
return readRequest || p.Modify
|
||||
}
|
||||
|
||||
func (p *Permissions) Validate() error {
|
||||
for _, r := range p.Rules {
|
||||
if err := r.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid permissions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Permissions `mapstructure:",squash"`
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (u User) checkPassword(input string) bool {
|
||||
if strings.HasPrefix(u.Password, "{bcrypt}") {
|
||||
savedPassword := strings.TrimPrefix(u.Password, "{bcrypt}")
|
||||
return bcrypt.CompareHashAndPassword([]byte(savedPassword), []byte(input)) == nil
|
||||
}
|
||||
|
||||
return u.Password == input
|
||||
}
|
||||
|
||||
func (u *User) Validate() error {
|
||||
if u.Username == "" {
|
||||
return errors.New("invalid user: username must be set")
|
||||
}
|
||||
|
||||
if u.Password == "" {
|
||||
return fmt.Errorf("invalid user %q: password must be set", u.Username)
|
||||
} else if strings.HasPrefix(u.Password, "{env}") {
|
||||
|
||||
env := strings.TrimPrefix(u.Password, "{env}")
|
||||
if env == "" {
|
||||
return fmt.Errorf("invalid user %q: password environment variable not set", u.Username)
|
||||
}
|
||||
|
||||
u.Password = os.Getenv(env)
|
||||
if u.Password == "" {
|
||||
return fmt.Errorf("invalid user %q: password environment variable is empty", u.Username)
|
||||
}
|
||||
}
|
||||
|
||||
if err := u.Permissions.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid user %q: %w", u.Username, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/hacdias/webdav/v4/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
# webdav
|
||||
|
||||
[](https://travis-ci.org/hacdias/webdav)
|
||||
[](https://goreportcard.com/report/hacdias/webdav)
|
||||
|
||||
```webdav``` is a simple tool that creates a WebDAV server for you. By default, it runs on a random free port and supports JSON and YAML configuration. Here is a simple YAML configuration example:
|
||||
|
||||
```yaml
|
||||
scope: /path/to/files
|
||||
address: 0.0.0.0
|
||||
port: 8080
|
||||
users:
|
||||
- username: admin
|
||||
password: admin
|
||||
- username: encrypted
|
||||
password: "{bcrypt}$2y$10$zEP6oofmXFeHaeMfBNLnP.DO8m.H.Mwhd24/TOX2MWLxAExXi4qgi"
|
||||
- username: basic
|
||||
password: basic
|
||||
modify: false
|
||||
rules:
|
||||
- regex: false
|
||||
- allow: false
|
||||
- path: /some/file
|
||||
```
|
||||
|
||||
You can specify the path to the configuration file using the `--config` flag. By default, it will search for a `config.{yaml,json}` file on your current working directory.
|
||||
|
||||
Download it [here](https://github.com/hacdias/webdav/releases).
|
||||
@@ -1,129 +0,0 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
// Config is the configuration of a WebDAV instance.
|
||||
type Config struct {
|
||||
*User
|
||||
Users map[string]*User
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
u := c.User
|
||||
|
||||
// Gets the correct user for this request.
|
||||
username, _, ok := r.BasicAuth()
|
||||
if ok {
|
||||
if user, ok := c.Users[username]; ok {
|
||||
u = user
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for user permissions relatively to this PATH.
|
||||
if !u.Allowed(r.URL.Path) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "HEAD" {
|
||||
w = newResponseWriterNoBody(w)
|
||||
}
|
||||
|
||||
// If this request modified the files and the user doesn't have permission
|
||||
// to do so, return forbidden.
|
||||
if (r.Method == "PUT" || r.Method == "POST" || r.Method == "MKCOL" ||
|
||||
r.Method == "DELETE" || r.Method == "COPY" || r.Method == "MOVE") &&
|
||||
!u.Modify {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Excerpt from RFC4918, section 9.4:
|
||||
//
|
||||
// GET, when applied to a collection, may return the contents of an
|
||||
// "index.html" resource, a human-readable view of the contents of
|
||||
// the collection, or something else altogether.
|
||||
//
|
||||
// Get, when applied to collection, will return the same as PROPFIND method.
|
||||
if r.Method == "GET" {
|
||||
info, err := u.Handler.FileSystem.Stat(context.TODO(), r.URL.Path)
|
||||
if err == nil && info.IsDir() {
|
||||
r.Method = "PROPFIND"
|
||||
}
|
||||
}
|
||||
|
||||
// Runs the WebDAV.
|
||||
u.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Rule is a dissalow/allow rule.
|
||||
type Rule struct {
|
||||
Regex bool
|
||||
Allow bool
|
||||
Path string
|
||||
Regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// User contains the settings of each user.
|
||||
type User struct {
|
||||
Scope string
|
||||
Modify bool
|
||||
Rules []*Rule
|
||||
Handler *webdav.Handler
|
||||
}
|
||||
|
||||
// Allowed checks if the user has permission to access a directory/file
|
||||
func (u User) Allowed(url string) bool {
|
||||
var rule *Rule
|
||||
i := len(u.Rules) - 1
|
||||
|
||||
for i >= 0 {
|
||||
rule = u.Rules[i]
|
||||
|
||||
if rule.Regex {
|
||||
if rule.Regexp.MatchString(url) {
|
||||
return rule.Allow
|
||||
}
|
||||
} else if strings.HasPrefix(url, rule.Path) {
|
||||
return rule.Allow
|
||||
}
|
||||
|
||||
i--
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// responseWriterNoBody is a wrapper used to suprress 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}
|
||||
}
|
||||
|
||||
// Header executes the Header method from the http.ResponseWriter.
|
||||
func (w responseWriterNoBody) Header() http.Header {
|
||||
return w.ResponseWriter.Header()
|
||||
}
|
||||
|
||||
// Write suprresses 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.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[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