mirror of
https://github.com/hacdias/webdav.git
synced 2026-09-23 03:31:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd472b26be | ||
|
|
7358553e69 | ||
|
|
764a69cd33 | ||
|
|
d266f1150e | ||
|
|
76ebaffaef | ||
|
|
60f2697615 | ||
|
|
e5b3946388 | ||
|
|
8c66f0c585 | ||
|
|
bd7e2d8158 | ||
|
|
552c72987d | ||
|
|
e022bad4bd | ||
|
|
82b3da92bd | ||
|
|
0b1da0c3c4 | ||
|
|
62ca86bf4e | ||
|
|
5afeb0eb60 | ||
|
|
4bb814bed6 | ||
|
|
c5f3907994 | ||
|
|
e81638120b | ||
|
|
f172f1bca1 | ||
|
|
24fa8aa228 | ||
|
|
78ceb0e44c | ||
|
|
5239649127 | ||
|
|
35fd913321 | ||
|
|
dd4a57af2e |
@@ -11,7 +11,7 @@ jobs:
|
|||||||
- image: circleci/golang:1.12
|
- image: circleci/golang:1.12
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run: cd cmd/webdav && go build main.go
|
- run: go build main.go
|
||||||
release:
|
release:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.12
|
- image: circleci/golang:1.12
|
||||||
|
|||||||
+15
-2
@@ -1,6 +1,15 @@
|
|||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod download
|
||||||
|
|
||||||
build:
|
build:
|
||||||
main: cmd/webdav/main.go
|
main: main.go
|
||||||
binary: webdav
|
binary: webdav
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/hacdias/webdav/cmd.version={{.Version}}
|
||||||
goos:
|
goos:
|
||||||
- darwin
|
- darwin
|
||||||
- linux
|
- linux
|
||||||
@@ -13,6 +22,10 @@ build:
|
|||||||
- 386
|
- 386
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
|
goarm:
|
||||||
|
- 5
|
||||||
|
- 6
|
||||||
|
- 7
|
||||||
ignore:
|
ignore:
|
||||||
- goos: openbsd
|
- goos: openbsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
@@ -23,7 +36,7 @@ build:
|
|||||||
|
|
||||||
archives:
|
archives:
|
||||||
-
|
-
|
||||||
name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}"
|
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||||
format: tar.gz
|
format: tar.gz
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# webdav
|
||||||
|
|
||||||
|
[](https://circleci.com/gh/hacdias/webdav)
|
||||||
|
[](https://goreportcard.com/report/hacdias/webdav)
|
||||||
|
[](https://github.com/hacdias/webdav/releases/latest)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT © [Henrique Dias](https://hacdias.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hacdias/webdav/v2/webdav"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
v "github.com/spf13/viper"
|
||||||
|
wd "golang.org/x/net/webdav"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseRules(raw []interface{}) []*webdav.Rule {
|
||||||
|
rules := []*webdav.Rule{}
|
||||||
|
|
||||||
|
for _, v := range raw {
|
||||||
|
if r, ok := v.(map[interface{}]interface{}); ok {
|
||||||
|
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["path"].(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Regex {
|
||||||
|
rule.Regexp = regexp.MustCompile(path)
|
||||||
|
} else {
|
||||||
|
rule.Path = path
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFromEnv(v string) (string, error) {
|
||||||
|
v = strings.TrimPrefix(v, "{env}")
|
||||||
|
if v == "" {
|
||||||
|
return "", errors.New("no environment variable specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
v = os.Getenv(v)
|
||||||
|
if v == "" {
|
||||||
|
return "", errors.New("the environment variable is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUsers(raw []interface{}, c *webdav.Config) {
|
||||||
|
var err error
|
||||||
|
for _, v := range raw {
|
||||||
|
if u, ok := v.(map[interface{}]interface{}); ok {
|
||||||
|
username, ok := u["username"].(string)
|
||||||
|
if !ok {
|
||||||
|
log.Fatal("user needs an username")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(username, "{env}") {
|
||||||
|
username, err = loadFromEnv(username)
|
||||||
|
checkErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
password, ok := u["password"].(string)
|
||||||
|
if !ok {
|
||||||
|
password = ""
|
||||||
|
|
||||||
|
if numPwd, ok := u["password"].(int); ok {
|
||||||
|
password = strconv.Itoa(numPwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(password, "{env}") {
|
||||||
|
password, err = loadFromEnv(password)
|
||||||
|
checkErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &webdav.User{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
Scope: c.User.Scope,
|
||||||
|
Modify: c.User.Modify,
|
||||||
|
Rules: c.User.Rules,
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope, ok := u["scope"].(string); ok {
|
||||||
|
user.Scope = scope
|
||||||
|
}
|
||||||
|
|
||||||
|
if modify, ok := u["modify"].(bool); ok {
|
||||||
|
user.Modify = modify
|
||||||
|
}
|
||||||
|
|
||||||
|
if rules, ok := u["rules"].([]interface{}); ok {
|
||||||
|
user.Rules = parseRules(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Handler = &wd.Handler{
|
||||||
|
FileSystem: wd.Dir(user.Scope),
|
||||||
|
LockSystem: wd.NewMemLS(),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Users[username] = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCors(cfg map[string]interface{}, c *webdav.Config) {
|
||||||
|
cors := webdav.CorsCfg{
|
||||||
|
Enabled: cfg["enabled"].(bool),
|
||||||
|
Credentials: cfg["credentials"].(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
cors.AllowedHeaders = corsProperty("allowed_headers", cfg)
|
||||||
|
cors.AllowedHosts = corsProperty("allowed_hosts", cfg)
|
||||||
|
cors.AllowedMethods = corsProperty("allowed_methods", cfg)
|
||||||
|
cors.ExposedHeaders = corsProperty("exposed_headers", cfg)
|
||||||
|
|
||||||
|
c.Cors = cors
|
||||||
|
}
|
||||||
|
|
||||||
|
func corsProperty(property string, cfg map[string]interface{}) []string {
|
||||||
|
var def []string
|
||||||
|
|
||||||
|
if property == "exposed_headers" {
|
||||||
|
def = []string{}
|
||||||
|
} else {
|
||||||
|
def = []string{"*"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowed, ok := cfg[property].([]interface{}); ok {
|
||||||
|
items := make([]string, len(allowed))
|
||||||
|
|
||||||
|
for idx, a := range allowed {
|
||||||
|
items[idx] = a.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
return def
|
||||||
|
} else {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfig(flags *pflag.FlagSet) *webdav.Config {
|
||||||
|
cfg := &webdav.Config{
|
||||||
|
User: &webdav.User{
|
||||||
|
Scope: getOpt(flags, "scope"),
|
||||||
|
Modify: getOptB(flags, "modify"),
|
||||||
|
Rules: []*webdav.Rule{},
|
||||||
|
Handler: &wd.Handler{
|
||||||
|
FileSystem: wd.Dir(getOpt(flags, "scope")),
|
||||||
|
LockSystem: wd.NewMemLS(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Auth: getOptB(flags, "auth"),
|
||||||
|
Cors: webdav.CorsCfg{
|
||||||
|
Enabled: false,
|
||||||
|
Credentials: false,
|
||||||
|
},
|
||||||
|
Users: map[string]*webdav.User{},
|
||||||
|
}
|
||||||
|
|
||||||
|
rawRules := v.Get("rules")
|
||||||
|
if rules, ok := rawRules.([]interface{}); ok {
|
||||||
|
cfg.User.Rules = parseRules(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawUsers := v.Get("users")
|
||||||
|
if users, ok := rawUsers.([]interface{}); ok {
|
||||||
|
parseUsers(users, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawCors := v.Get("cors")
|
||||||
|
if cors, ok := rawCors.(map[string]interface{}); ok {
|
||||||
|
parseCors(cors, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Users) != 0 && !cfg.Auth {
|
||||||
|
log.Print("Users will be ignored due to auth=false")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
v "github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfgFile string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cobra.OnInitialize(initConfig)
|
||||||
|
|
||||||
|
flags := rootCmd.Flags()
|
||||||
|
flags.StringVarP(&cfgFile, "config", "c", "", "config file path")
|
||||||
|
flags.BoolP("tls", "t", false, "enable tls")
|
||||||
|
flags.Bool("auth", true, "enable auth")
|
||||||
|
flags.String("cert", "cert.pem", "TLS certificate")
|
||||||
|
flags.String("key", "key.pem", "TLS key")
|
||||||
|
flags.StringP("address", "a", "0.0.0.0", "address to listen to")
|
||||||
|
flags.StringP("port", "p", "0", "port to listen to")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
cfg := readConfig(flags)
|
||||||
|
|
||||||
|
// Builds the address and a listener.
|
||||||
|
laddr := getOpt(flags, "address") + ":" + getOpt(flags, "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 getOptB(flags, "tls") {
|
||||||
|
if err := http.ServeTLS(listener, cfg, getOpt(flags, "cert"), getOpt(flags, "key")); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := http.Serve(listener, cfg); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConfig() {
|
||||||
|
if cfgFile == "" {
|
||||||
|
v.AddConfigPath(".")
|
||||||
|
v.AddConfigPath("/etc/webdav/")
|
||||||
|
v.SetConfigName("config")
|
||||||
|
} else {
|
||||||
|
v.SetConfigFile(cfgFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.SetEnvPrefix("WD")
|
||||||
|
v.AutomaticEnv()
|
||||||
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
|
||||||
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(v.ConfigParseError); ok {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cfgFile = "No config file used"
|
||||||
|
} else {
|
||||||
|
cfgFile = "Using config file: " + v.ConfigFileUsed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Println("WebDAV version " + version)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
v "github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getOption returns a parameter as a string.
|
||||||
|
//
|
||||||
|
// NOTE: we could simply bind the flags to viper and use IsSet.
|
||||||
|
// Although there is a bug on Viper that always returns true on IsSet
|
||||||
|
// if a flag is binded. Our alternative way is to manually check
|
||||||
|
// the flag and then the value from env/config/gotten by viper.
|
||||||
|
// https://github.com/spf13/viper/pull/331
|
||||||
|
func getOpt(flags *pflag.FlagSet, key string) string {
|
||||||
|
value, _ := flags.GetString(key)
|
||||||
|
|
||||||
|
// If set on Flags, use it.
|
||||||
|
if flags.Changed(key) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// If set through viper (env, config), return it.
|
||||||
|
if v.IsSet(key) {
|
||||||
|
return v.GetString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use default value on flags.
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOptB(flags *pflag.FlagSet, key string) bool {
|
||||||
|
value, _ := flags.GetBool(key)
|
||||||
|
|
||||||
|
// If set on Flags, use it.
|
||||||
|
if flags.Changed(key) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// If set through viper (env, config), return it.
|
||||||
|
if v.IsSet(key) {
|
||||||
|
return v.GetBool(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use default value on flags.
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,12 @@
|
|||||||
module github.com/hacdias/webdav
|
module github.com/hacdias/webdav/v2
|
||||||
|
|
||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/hacdias/webdav v1.4.1 // indirect
|
||||||
|
github.com/spf13/cobra v0.0.3
|
||||||
|
github.com/spf13/pflag v1.0.3
|
||||||
|
github.com/spf13/viper v1.3.2
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529
|
||||||
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5
|
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,57 @@
|
|||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/hacdias/webdav v1.4.1 h1:yhR4dgKr3JUfgDhFO2nWTq3HiYA37Sk/kaud/bRx1io=
|
||||||
|
github.com/hacdias/webdav v1.4.1/go.mod h1:1qXJOCQfiAXgYonkWHIMgWSfbKqUW8F4DlzXxSJCYIg=
|
||||||
|
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.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
||||||
|
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak=
|
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak=
|
||||||
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/hacdias/webdav/v2/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
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).
|
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package webdav
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/webdav"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
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
|
||||||
|
}
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package webdav
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 isAllowedHost(allowedHosts []string, origin string) bool {
|
||||||
|
for _, host := range allowedHosts {
|
||||||
|
if host == origin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Regular → Executable
+82
-46
@@ -2,28 +2,102 @@ package webdav
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/net/webdav"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CorsCfg is the CORS config.
|
||||||
|
type CorsCfg struct {
|
||||||
|
Enabled bool
|
||||||
|
Credentials bool
|
||||||
|
AllowedHeaders []string
|
||||||
|
AllowedHosts []string
|
||||||
|
AllowedMethods []string
|
||||||
|
ExposedHeaders []string
|
||||||
|
}
|
||||||
|
|
||||||
// Config is the configuration of a WebDAV instance.
|
// Config is the configuration of a WebDAV instance.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
*User
|
*User
|
||||||
|
Auth bool
|
||||||
|
Cors CorsCfg
|
||||||
Users map[string]*User
|
Users map[string]*User
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
// 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) {
|
func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
u := c.User
|
u := c.User
|
||||||
|
requestOrigin := r.Header.Get("Origin")
|
||||||
|
|
||||||
// Gets the correct user for this request.
|
// Add CORS headers before any operation so even on a 401 unauthorized status, CORS will work.
|
||||||
username, _, ok := r.BasicAuth()
|
if c.Cors.Enabled && requestOrigin != "" {
|
||||||
if ok {
|
headers := w.Header()
|
||||||
if user, ok := c.Users[username]; ok {
|
|
||||||
u = user
|
allowedHeaders := strings.Join(c.Cors.AllowedHeaders, ", ")
|
||||||
|
allowedMethods := strings.Join(c.Cors.AllowedMethods, ", ")
|
||||||
|
exposedHeaders := strings.Join(c.Cors.ExposedHeaders, ", ")
|
||||||
|
|
||||||
|
allowAllHosts := len(c.Cors.AllowedHosts) == 1 && c.Cors.AllowedHosts[0] == "*"
|
||||||
|
allowedHost := isAllowedHost(c.Cors.AllowedHosts, requestOrigin)
|
||||||
|
|
||||||
|
if allowAllHosts {
|
||||||
|
headers.Set("Access-Control-Allow-Origin", "*")
|
||||||
|
} else if allowedHost {
|
||||||
|
headers.Set("Access-Control-Allow-Origin", requestOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowAllHosts || allowedHost {
|
||||||
|
headers.Set("Access-Control-Allow-Headers", allowedHeaders)
|
||||||
|
headers.Set("Access-Control-Allow-Methods", allowedMethods)
|
||||||
|
|
||||||
|
if c.Cors.Credentials {
|
||||||
|
headers.Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Cors.ExposedHeaders) > 0 {
|
||||||
|
headers.Set("Access-Control-Expose-Headers", exposedHeaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" && c.Cors.Enabled && requestOrigin != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
if c.Auth {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||||
|
|
||||||
|
// Gets the correct user for this request.
|
||||||
|
username, password, ok := r.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Not authorized", 401)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, ok := c.Users[username]
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Not authorized", 401)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkPassword(user.Password, password) {
|
||||||
|
log.Println("Wrong Password for user", username)
|
||||||
|
http.Error(w, "Not authorized", 401)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u = user
|
||||||
|
} else {
|
||||||
|
// Even if Auth is disabled, we might want to get
|
||||||
|
// the user from the Basic Auth header. Useful for Caddy
|
||||||
|
// plugin implementation.
|
||||||
|
username, _, ok := r.BasicAuth()
|
||||||
|
if ok {
|
||||||
|
if user, ok := c.Users[username]; ok {
|
||||||
|
u = user
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,44 +142,6 @@ func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
u.Handler.ServeHTTP(w, r)
|
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
|
// responseWriterNoBody is a wrapper used to suprress the body of the response
|
||||||
// to a request. Mainly used for HEAD requests.
|
// to a request. Mainly used for HEAD requests.
|
||||||
type responseWriterNoBody struct {
|
type responseWriterNoBody struct {
|
||||||
Reference in New Issue
Block a user