Skip to content

Commit

Permalink
wipo
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Jul 19, 2024
1 parent 8dfc727 commit 8e41edc
Show file tree
Hide file tree
Showing 7 changed files with 399 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
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/rs/cors v1.11.0 // 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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
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=
Expand Down
93 changes: 93 additions & 0 deletions new/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package webdav

import (
"strings"

"github.com/spf13/viper"
"golang.org/x/crypto/bcrypt"
)

type User struct {
Permissions
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
}

type CORS struct {
Enabled bool
Credentials bool
AllowedHeaders []string
AllowedHosts []string
AllowedMethods []string
ExposedHeaders []string
}

type Config struct {
Permissions
Auth bool
TLS bool
Cert string
Key string
Prefix string
Debug bool
NoSniff bool
LogFormat string
CORS CORS
Users []User
}

func ParseConfig(filename string) (*Config, error) {
v := viper.New()

// 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{"*"})

// TODO: bind flags

err := v.ReadInConfig()
if err != nil {
return nil, err
}

cfg := &Config{}
err = v.Unmarshal(cfg)
if err != nil {
return nil, err
}

err = cfg.Validate()
if err != nil {
return nil, err
}

return cfg, nil
}

func (c *Config) Validate() error {

return nil
}
82 changes: 82 additions & 0 deletions new/files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package webdav

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 changes: 133 additions & 0 deletions new/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package webdav

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)
}
61 changes: 61 additions & 0 deletions new/permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package webdav

import (
"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
}

// 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
}
Loading

0 comments on commit 8e41edc

Please sign in to comment.