diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 63339e38..00000000 --- a/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# EditorConfig is awesome: http://EditorConfig.org - -# top-most EditorConfig file -root = true - -# Unix-style newlines with a newline ending every file -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -# 4 space indentation -[*.go] -indent_style = tab -indent_size = 4 diff --git a/config/commands.go b/config/commands.go deleted file mode 100644 index 60c3b881..00000000 --- a/config/commands.go +++ /dev/null @@ -1,62 +0,0 @@ -package config - -import ( - "log" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/mholt/caddy" -) - -// CommandFunc ... -type CommandFunc func(r *http.Request, c *Config, u *User) error - -// CommandRunner ... -func CommandRunner(c *caddy.Controller) (CommandFunc, error) { - fn := func(r *http.Request, c *Config, u *User) error { return nil } - - args := c.RemainingArgs() - if len(args) == 0 { - return fn, c.ArgErr() - } - - nonblock := false - if len(args) > 1 && args[len(args)-1] == "&" { - // Run command in background; non-blocking - nonblock = true - args = args[:len(args)-1] - } - - command, args, err := caddy.SplitCommandAndArgs(strings.Join(args, " ")) - if err != nil { - return fn, c.Err(err.Error()) - } - - fn = func(r *http.Request, c *Config, u *User) error { - path := strings.Replace(r.URL.Path, c.WebDavURL, "", 1) - path = u.Scope + "/" + path - path = filepath.Clean(path) - - for i := range args { - args[i] = strings.Replace(args[i], "{path}", path, -1) - } - - cmd := exec.Command(command, args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if nonblock { - log.Printf("[INFO] Nonblocking Command:\"%s %s\"", command, strings.Join(args, " ")) - return cmd.Start() - } - - log.Printf("[INFO] Blocking Command:\"%s %s\"", command, strings.Join(args, " ")) - return cmd.Run() - } - - return fn, nil -} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index bfad2ad7..00000000 --- a/config/config.go +++ /dev/null @@ -1,261 +0,0 @@ -package config - -import ( - "fmt" - "io/ioutil" - "net/http" - "regexp" - "strconv" - "strings" - - "golang.org/x/net/webdav" - - "github.com/mholt/caddy" - "github.com/mholt/caddy/caddyhttp/httpserver" -) - -// Config is a configuration for browsing in a particular path. -type Config struct { - *User - PrefixURL string - BaseURL string - WebDavURL string - HugoEnabled bool // Enables the Hugo plugin for File Manager - Users map[string]*User - BeforeSave CommandFunc - AfterSave CommandFunc -} - -// AbsoluteURL ... -func (c Config) AbsoluteURL() string { - return c.PrefixURL + c.BaseURL -} - -// AbsoluteWebdavURL ... -func (c Config) AbsoluteWebdavURL() string { - return c.PrefixURL + c.WebDavURL -} - -// Rule is a dissalow/allow rule -type Rule struct { - Regex bool - Allow bool - Path string - Regexp *regexp.Regexp -} - -// Parse parses the configuration set by the user so it can -// be used by the middleware -func Parse(c *caddy.Controller) ([]Config, error) { - var ( - configs []Config - err error - user *User - ) - - appendConfig := func(cfg Config) error { - for _, c := range configs { - if c.Scope == cfg.Scope { - return fmt.Errorf("duplicate file managing config for %s", c.Scope) - } - } - configs = append(configs, cfg) - return nil - } - - for c.Next() { - // Initialize the configuration with the default settings - cfg := Config{User: &User{}} - cfg.Scope = "." - cfg.FileSystem = webdav.Dir(cfg.Scope) - cfg.BaseURL = "" - cfg.HugoEnabled = false - cfg.Users = map[string]*User{} - cfg.AllowCommands = true - cfg.AllowEdit = true - cfg.AllowNew = true - cfg.Commands = []string{"git", "svn", "hg"} - cfg.BeforeSave = func(r *http.Request, c *Config, u *User) error { return nil } - cfg.AfterSave = func(r *http.Request, c *Config, u *User) error { return nil } - cfg.Rules = []*Rule{{ - Regex: true, - Allow: false, - Regexp: regexp.MustCompile("\\/\\..+"), - }} - - // Get the baseURL - args := c.RemainingArgs() - - if len(args) > 0 { - cfg.BaseURL = args[0] - } - - cfg.BaseURL = strings.TrimPrefix(cfg.BaseURL, "/") - cfg.BaseURL = strings.TrimSuffix(cfg.BaseURL, "/") - cfg.BaseURL = "/" + cfg.BaseURL - cfg.WebDavURL = "" - - if cfg.BaseURL == "/" { - cfg.BaseURL = "" - } - - // Set the first user, the global user - user = cfg.User - - for c.NextBlock() { - switch c.Val() { - case "before_save": - if cfg.BeforeSave, err = CommandRunner(c); err != nil { - return configs, err - } - case "after_save": - if cfg.AfterSave, err = CommandRunner(c); err != nil { - return configs, err - } - case "webdav": - if !c.NextArg() { - return configs, c.ArgErr() - } - - prefix := c.Val() - prefix = strings.TrimPrefix(prefix, "/") - prefix = strings.TrimSuffix(prefix, "/") - cfg.WebDavURL = prefix - case "show": - if !c.NextArg() { - return configs, c.ArgErr() - } - - user.Scope = c.Val() - user.Scope = strings.TrimSuffix(user.Scope, "/") - user.FileSystem = webdav.Dir(user.Scope) - case "styles": - if !c.NextArg() { - return configs, c.ArgErr() - } - - var tplBytes []byte - tplBytes, err = ioutil.ReadFile(c.Val()) - if err != nil { - return configs, err - } - user.StyleSheet = string(tplBytes) - case "allow_new": - if !c.NextArg() { - return configs, c.ArgErr() - } - - user.AllowNew, err = strconv.ParseBool(c.Val()) - if err != nil { - return configs, err - } - case "allow_edit": - if !c.NextArg() { - return configs, c.ArgErr() - } - - user.AllowEdit, err = strconv.ParseBool(c.Val()) - if err != nil { - return configs, err - } - case "allow_commands": - if !c.NextArg() { - return configs, c.ArgErr() - } - - user.AllowCommands, err = strconv.ParseBool(c.Val()) - if err != nil { - return configs, err - } - case "allow_command": - if !c.NextArg() { - return configs, c.ArgErr() - } - - user.Commands = append(user.Commands, c.Val()) - case "block_command": - if !c.NextArg() { - return configs, c.ArgErr() - } - - index := 0 - - for i, val := range user.Commands { - if val == c.Val() { - index = i - } - } - - user.Commands = append(user.Commands[:index], user.Commands[index+1:]...) - case "allow", "allow_r", "block", "block_r": - ruleType := c.Val() - - if !c.NextArg() { - return configs, c.ArgErr() - } - - if c.Val() == "dotfiles" && !strings.HasSuffix(ruleType, "_r") { - ruleType += "_r" - } - - rule := &Rule{ - Allow: ruleType == "allow" || ruleType == "allow_r", - Regex: ruleType == "allow_r" || ruleType == "block_r", - } - - if rule.Regex && c.Val() == "dotfiles" { - rule.Regexp = regexp.MustCompile("\\/\\..+") - } else if rule.Regex { - rule.Regexp = regexp.MustCompile(c.Val()) - } else { - rule.Path = c.Val() - } - - user.Rules = append(user.Rules, rule) - // NEW USER BLOCK? - default: - val := c.Val() - - // Checks if it's a new user - if !strings.HasSuffix(val, ":") { - fmt.Println("Unknown option " + val) - } - - // Get the username, sets the current user, and initializes it - val = strings.TrimSuffix(val, ":") - cfg.Users[val] = &User{} - - // Initialize the new user - user = cfg.Users[val] - user.AllowCommands = cfg.AllowCommands - user.AllowEdit = cfg.AllowEdit - user.AllowNew = cfg.AllowEdit - user.Commands = cfg.Commands - user.Scope = cfg.Scope - user.FileSystem = cfg.FileSystem - user.Rules = cfg.Rules - user.StyleSheet = cfg.StyleSheet - } - } - - if cfg.WebDavURL == "" { - cfg.WebDavURL = "webdav" - } - - caddyConf := httpserver.GetConfig(c) - - cfg.PrefixURL = strings.TrimSuffix(caddyConf.Addr.Path, "/") - cfg.WebDavURL = cfg.BaseURL + "/" + strings.TrimPrefix(cfg.WebDavURL, "/") - cfg.Handler = &webdav.Handler{ - Prefix: cfg.WebDavURL, - FileSystem: cfg.FileSystem, - LockSystem: webdav.NewMemLS(), - } - - if err := appendConfig(cfg); err != nil { - return configs, err - } - } - - return configs, nil -} diff --git a/config/user.go b/config/user.go deleted file mode 100644 index 4f4f47f6..00000000 --- a/config/user.go +++ /dev/null @@ -1,42 +0,0 @@ -package config - -import ( - "strings" - - "golang.org/x/net/webdav" -) - -// User contains the configuration for each user -type User struct { - Scope string `json:"-"` // Path the user have access - FileSystem webdav.FileSystem `json:"-"` // The virtual file system the user have access - Handler *webdav.Handler `json:"-"` // The WebDav HTTP Handler - StyleSheet string `json:"-"` // Costum stylesheet - AllowNew bool // Can create files and folders - AllowEdit bool // Can edit/rename files - AllowCommands bool // Can execute commands - Commands []string // Available Commands - Rules []*Rule `json:"-"` // Access rules -} - -// 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 -} diff --git a/filemanager.go b/filemanager.go index a17983a8..41d48a9a 100644 --- a/filemanager.go +++ b/filemanager.go @@ -4,18 +4,9 @@ package filemanager import ( - e "errors" "net/http" - "os" - "path/filepath" - "strings" - "github.com/hacdias/caddy-filemanager/assets" - "github.com/hacdias/caddy-filemanager/config" - "github.com/hacdias/caddy-filemanager/file" - "github.com/hacdias/caddy-filemanager/handlers" - "github.com/hacdias/caddy-filemanager/page" - "github.com/hacdias/caddy-filemanager/wrapper" + "github.com/hacdias/filemanager" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -23,173 +14,18 @@ import ( // directories in the given paths are specified. type FileManager struct { Next httpserver.Handler - Configs []config.Config + Configs []*filemanager.FileManager } // ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - var ( - c *config.Config - fi *file.Info - code int - err error - user *config.User - ) - for i := range f.Configs { // Checks if this Path should be handled by File Manager. if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { continue } - c = &f.Configs[i] - - // Checks if the URL matches the Assets URL. Returns the asset if the - // method is GET and Status Forbidden otherwise. - if httpserver.Path(r.URL.Path).Matches(c.BaseURL + assets.BaseURL) { - if r.Method == http.MethodGet { - return assets.Serve(w, r, c) - } - - return http.StatusForbidden, nil - } - - // Obtains the user. See https://github.com/mholt/caddy/blob/master/caddyhttp/basicauth/basicauth.go#L66 - username, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string) - if _, ok := c.Users[username]; ok { - user = c.Users[username] - } else { - user = c.User - } - - // Checks if the request URL is for the WebDav server - if httpserver.Path(r.URL.Path).Matches(c.WebDavURL) { - // Checks for user permissions relatively to this PATH - if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) { - return http.StatusForbidden, nil - } - - switch r.Method { - case "GET", "HEAD": - // 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. - // - // It was decided on https://github.com/hacdias/caddy-filemanager/issues/85 - // that GET, for collections, will return the same as PROPFIND method. - path := strings.Replace(r.URL.Path, c.WebDavURL, "", 1) - path = user.Scope + "/" + path - path = filepath.Clean(path) - - var i os.FileInfo - i, err = os.Stat(path) - if err != nil { - // Is there any error? WebDav will handle it... no worries. - break - } - - if i.IsDir() { - r.Method = "PROPFIND" - - if r.Method == "HEAD" { - w = wrapper.NewResponseWriterNoBody(w) - } - } - case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE": - if !user.AllowEdit { - return http.StatusForbidden, nil - } - case "MKCOL", "COPY": - if !user.AllowNew { - return http.StatusForbidden, nil - } - } - - // Preprocess the PUT request if it's the case - if r.Method == http.MethodPut { - if err = c.BeforeSave(r, c, user); err != nil { - return http.StatusInternalServerError, err - } - - if handlers.PreProccessPUT(w, r, c, user) != nil { - return http.StatusInternalServerError, err - } - } - - c.Handler.ServeHTTP(w, r) - if err = c.AfterSave(r, c, user); err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil - } - - w.Header().Set("x-frame-options", "SAMEORIGIN") - w.Header().Set("x-content-type", "nosniff") - w.Header().Set("x-xss-protection", "1; mode=block") - - // Checks if the User is allowed to access this file - if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.BaseURL)) { - if r.Method == http.MethodGet { - return page.PrintErrorHTML( - w, http.StatusForbidden, - e.New("You don't have permission to access this page."), - ) - } - - return http.StatusForbidden, nil - } - - if r.URL.Query().Get("search") != "" { - return handlers.Search(w, r, c, user) - } - - if r.URL.Query().Get("command") != "" { - return handlers.Command(w, r, c, user) - } - - if r.Method == http.MethodGet { - // Gets the information of the directory/file - fi, code, err = file.GetInfo(r.URL, c, user) - if err != nil { - if r.Method == http.MethodGet { - return page.PrintErrorHTML(w, code, err) - } - return code, err - } - - // If it's a dir and the path doesn't end with a trailing slash, - // redirect the user. - if fi.IsDir && !strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, c.PrefixURL+r.URL.Path+"/", http.StatusTemporaryRedirect) - return 0, nil - } - - switch { - case r.URL.Query().Get("download") != "": - code, err = handlers.Download(w, r, c, fi) - case r.URL.Query().Get("raw") == "true" && !fi.IsDir: - http.ServeFile(w, r, fi.Path) - code, err = 0, nil - case !fi.IsDir && r.URL.Query().Get("checksum") != "": - code, err = handlers.Checksum(w, r, c, fi) - case fi.IsDir: - code, err = handlers.ServeListing(w, r, c, user, fi) - default: - code, err = handlers.ServeSingle(w, r, c, user, fi) - } - - if err != nil { - code, err = page.PrintErrorHTML(w, code, err) - } - - return code, err - } - - return http.StatusNotImplemented, nil - + return f.Configs[i].ServeHTTP(w, r) } return f.Next.ServeHTTP(w, r) diff --git a/setup.go b/setup.go index a7c97b1b..f491d875 100644 --- a/setup.go +++ b/setup.go @@ -1,7 +1,18 @@ package filemanager import ( - "github.com/hacdias/caddy-filemanager/config" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/hacdias/filemanager" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -15,7 +26,7 @@ func init() { // setup configures a new FileManager middleware instance. func setup(c *caddy.Controller) error { - configs, err := config.Parse(c) + configs, err := parse(c) if err != nil { return err } @@ -26,3 +37,199 @@ func setup(c *caddy.Controller) error { return nil } + +func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) { + var ( + configs []*filemanager.FileManager + err error + ) + + for c.Next() { + var ( + m = filemanager.New(".") + u = m.User + name = "" + ) + + // Get the baseURL + args := c.RemainingArgs() + + if len(args) > 0 { + m.SetBaseURL(args[0]) + m.SetWebDavURL("/webdav") + } + + for c.NextBlock() { + switch c.Val() { + case "before_save": + /* if cfg.BeforeSave, err = CommandRunner(c); err != nil { + return configs, err + } */ + case "after_save": + /* if cfg.AfterSave, err = CommandRunner(c); err != nil { + return configs, err + } */ + case "webdav": + if !c.NextArg() { + return configs, c.ArgErr() + } + + m.SetWebDavURL(c.Val()) + case "show": + if !c.NextArg() { + return configs, c.ArgErr() + } + + m.SetScope(c.Val(), name) + case "styles": + if !c.NextArg() { + return configs, c.ArgErr() + } + + var tplBytes []byte + tplBytes, err = ioutil.ReadFile(c.Val()) + if err != nil { + return configs, err + } + + u.StyleSheet = string(tplBytes) + case "allow_new": + if !c.NextArg() { + return configs, c.ArgErr() + } + + u.AllowNew, err = strconv.ParseBool(c.Val()) + if err != nil { + return configs, err + } + case "allow_edit": + if !c.NextArg() { + return configs, c.ArgErr() + } + + u.AllowEdit, err = strconv.ParseBool(c.Val()) + if err != nil { + return configs, err + } + case "allow_commands": + if !c.NextArg() { + return configs, c.ArgErr() + } + + u.AllowCommands, err = strconv.ParseBool(c.Val()) + if err != nil { + return configs, err + } + case "allow_command": + if !c.NextArg() { + return configs, c.ArgErr() + } + + u.Commands = append(u.Commands, c.Val()) + case "block_command": + if !c.NextArg() { + return configs, c.ArgErr() + } + + index := 0 + + for i, val := range u.Commands { + if val == c.Val() { + index = i + } + } + + u.Commands = append(u.Commands[:index], u.Commands[index+1:]...) + case "allow", "allow_r", "block", "block_r": + ruleType := c.Val() + + if !c.NextArg() { + return configs, c.ArgErr() + } + + if c.Val() == "dotfiles" && !strings.HasSuffix(ruleType, "_r") { + ruleType += "_r" + } + + rule := &filemanager.Rule{ + Allow: ruleType == "allow" || ruleType == "allow_r", + Regex: ruleType == "allow_r" || ruleType == "block_r", + } + + if rule.Regex && c.Val() == "dotfiles" { + rule.Regexp = regexp.MustCompile("\\/\\..+") + } else if rule.Regex { + rule.Regexp = regexp.MustCompile(c.Val()) + } else { + rule.Path = c.Val() + } + + u.Rules = append(u.Rules, rule) + default: + // Is it a new user? Is it? + val := c.Val() + + // Checks if it's a new user! + if !strings.HasSuffix(val, ":") { + fmt.Println("Unknown option " + val) + } + + // Get the username, sets the current user, and initializes it + val = strings.TrimSuffix(val, ":") + m.NewUser(val) + name = val + } + } + + configs = append(configs, m) + } + + return configs, nil +} + +// CommandRunner ... +func CommandRunner(c *caddy.Controller) (filemanager.Command, error) { + fn := func(r *http.Request, c *filemanager.FileManager, u *filemanager.User) error { return nil } + + args := c.RemainingArgs() + if len(args) == 0 { + return fn, c.ArgErr() + } + + nonblock := false + if len(args) > 1 && args[len(args)-1] == "&" { + // Run command in background; non-blocking + nonblock = true + args = args[:len(args)-1] + } + + command, args, err := caddy.SplitCommandAndArgs(strings.Join(args, " ")) + if err != nil { + return fn, c.Err(err.Error()) + } + + fn = func(r *http.Request, c *filemanager.FileManager, u *filemanager.User) error { + path := strings.Replace(r.URL.Path, c.WebDavURL, "", 1) + path = u.Scope() + "/" + path + path = filepath.Clean(path) + + for i := range args { + args[i] = strings.Replace(args[i], "{path}", path, -1) + } + + cmd := exec.Command(command, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if nonblock { + log.Printf("[INFO] Nonblocking Command:\"%s %s\"", command, strings.Join(args, " ")) + return cmd.Start() + } + + log.Printf("[INFO] Blocking Command:\"%s %s\"", command, strings.Join(args, " ")) + return cmd.Run() + } + + return fn, nil +}