mirror of
https://github.com/hacdias/webdav.git
synced 2026-09-23 03:31:54 +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 |
@@ -1,45 +0,0 @@
|
|||||||
version: 2
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
docker:
|
|
||||||
- image: golangci/golangci-lint:v1.16
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: golangci-lint run -v
|
|
||||||
build:
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:1.12
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: cd cmd/webdav && go build main.go
|
|
||||||
release:
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:1.12
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- setup_remote_docker
|
|
||||||
- run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
|
||||||
- run: curl -sL https://git.io/goreleaser | bash
|
|
||||||
- run: docker logout
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build-workflow:
|
|
||||||
jobs:
|
|
||||||
- lint:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /.*/
|
|
||||||
- build:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /.*/
|
|
||||||
- release:
|
|
||||||
context: deploy
|
|
||||||
requires:
|
|
||||||
- build
|
|
||||||
- lint
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /^v.*/
|
|
||||||
branches:
|
|
||||||
ignore: /.*/
|
|
||||||
@@ -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 ./...
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
webdav
|
||||||
+48
-34
@@ -1,41 +1,55 @@
|
|||||||
build:
|
version: 2
|
||||||
main: cmd/webdav/main.go
|
|
||||||
binary: webdav
|
before:
|
||||||
goos:
|
hooks:
|
||||||
- darwin
|
- go mod tidy
|
||||||
- linux
|
- go mod download
|
||||||
- windows
|
|
||||||
- freebsd
|
builds:
|
||||||
- netbsd
|
- main: main.go
|
||||||
- openbsd
|
binary: webdav
|
||||||
goarch:
|
flags:
|
||||||
- amd64
|
- '-trimpath'
|
||||||
- 386
|
ldflags:
|
||||||
- arm
|
- '-X github.com/hacdias/webdav/v4/cmd.version={{.Version}}'
|
||||||
- arm64
|
goos:
|
||||||
ignore:
|
- darwin
|
||||||
- goos: openbsd
|
- linux
|
||||||
goarch: arm
|
- windows
|
||||||
goarm: 6
|
- freebsd
|
||||||
- goos: freebsd
|
- netbsd
|
||||||
goarch: arm
|
- openbsd
|
||||||
goarm: 6
|
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:
|
archives:
|
||||||
-
|
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||||
name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}"
|
|
||||||
format: tar.gz
|
format: tar.gz
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
format: zip
|
||||||
|
|
||||||
dockers:
|
release:
|
||||||
-
|
github:
|
||||||
goos: linux
|
owner: hacdias
|
||||||
goarch: amd64
|
name: webdav
|
||||||
goarm: ''
|
draft: false
|
||||||
image_templates:
|
prerelease: auto
|
||||||
- "hacdias/webdav:latest"
|
|
||||||
- "hacdias/webdav:{{ .Tag }}"
|
|
||||||
- "hacdias/webdav:v{{ .Major }}.{{ .Minor }}"
|
|
||||||
- "hacdias/webdav:v{{ .Major }}"
|
|
||||||
|
|||||||
+18
-5
@@ -1,11 +1,24 @@
|
|||||||
FROM alpine:latest as certs
|
FROM golang:1.22-alpine3.20 AS build
|
||||||
|
|
||||||
|
ARG DOCKER_META_VERSION="untracked"
|
||||||
|
|
||||||
RUN apk --update add ca-certificates
|
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
|
FROM scratch
|
||||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
|
||||||
|
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 80
|
||||||
|
|
||||||
COPY webdav /webdav
|
ENTRYPOINT [ "webdav" ]
|
||||||
|
CMD [ "-p", "80" ]
|
||||||
ENTRYPOINT [ "/webdav" ]
|
|
||||||
|
|||||||
@@ -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,299 +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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// load username from environment when prefix {env} is added
|
|
||||||
if strings.HasPrefix(username, "{env}") {
|
|
||||||
var envUsername = strings.TrimPrefix(username, "{env}")
|
|
||||||
if envUsername == "" {
|
|
||||||
log.Fatal("no environment variable specified for username")
|
|
||||||
}
|
|
||||||
username = os.Getenv(envUsername)
|
|
||||||
if username == "" {
|
|
||||||
log.Fatal("username must be set in environment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
password, ok := r["password"].(string)
|
|
||||||
if !ok {
|
|
||||||
log.Fatal("user needs a password")
|
|
||||||
}
|
|
||||||
|
|
||||||
// load password from environment when prefix {env} is added
|
|
||||||
if strings.HasPrefix(password, "{env}") {
|
|
||||||
var envPassword = strings.TrimPrefix(password, "{env}")
|
|
||||||
if envPassword == "" {
|
|
||||||
log.Fatal("no environment variable specified for password")
|
|
||||||
}
|
|
||||||
password = os.Getenv(envPassword)
|
|
||||||
if password == "" {
|
|
||||||
log.Fatal("password must be set in environment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
tls bool
|
|
||||||
cert string
|
|
||||||
key string
|
|
||||||
auth map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseConfig() *cfg {
|
|
||||||
file := getConfig()
|
|
||||||
|
|
||||||
data := struct {
|
|
||||||
Address string `json:"address" yaml:"address"`
|
|
||||||
Port string `json:"port" yaml:"port"`
|
|
||||||
TLS bool `json:"tls" yaml:"tls"`
|
|
||||||
Cert string `json:"cert" yaml:"cert"`
|
|
||||||
Key string `json:"key" yaml:"key"`
|
|
||||||
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",
|
|
||||||
TLS: false,
|
|
||||||
Cert: "cert.pem",
|
|
||||||
Key: "key.pem",
|
|
||||||
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,
|
|
||||||
tls: data.TLS,
|
|
||||||
cert: data.Cert,
|
|
||||||
key: data.Key,
|
|
||||||
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 {
|
|
||||||
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 cfg.tls {
|
|
||||||
if err := http.ServeTLS(listener, handler, cfg.cert, cfg.key); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := http.Serve(listener, handler); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,39 @@
|
|||||||
module github.com/hacdias/webdav
|
module github.com/hacdias/webdav/v4
|
||||||
|
|
||||||
go 1.12
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/rs/cors v1.11.0
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529
|
github.com/spf13/cobra v1.8.1
|
||||||
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5
|
github.com/spf13/pflag v1.0.5
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
github.com/spf13/viper v1.19.0
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
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
|
||||||
|
|||||||
@@ -1,20 +1,84 @@
|
|||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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,35 +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
|
|
||||||
tls: false
|
|
||||||
cert: cert.pem
|
|
||||||
key: key.pem
|
|
||||||
users:
|
|
||||||
- username: admin
|
|
||||||
password: admin
|
|
||||||
- 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
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
An example of how to use this with `systemd` is on [webdav.service.example](/webdav.service.example).
|
|
||||||
|
|
||||||
Download it [here](https://github.com/hacdias/webdav/releases).
|
|
||||||
@@ -1,133 +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"
|
|
||||||
|
|
||||||
if r.Header.Get("Depth") == "" {
|
|
||||||
r.Header.Add("Depth", "1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user