-
-
Notifications
You must be signed in to change notification settings - Fork 466
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
399 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.