diff --git a/http_assets.go b/assets.go similarity index 72% rename from http_assets.go rename to assets.go index 57a23be6..8e27f5a3 100644 --- a/http_assets.go +++ b/assets.go @@ -22,26 +22,10 @@ func serveAssets(w http.ResponseWriter, r *http.Request, m *FileManager) (int, e switch { case strings.HasPrefix(filename, "/css"): filename = strings.Replace(filename, "/css/", "", 1) - - if m.Assets.CSS != nil { - file, err = m.Assets.CSS.Bytes(filename) - if err == nil { - break - } - } - - file, err = m.Assets.baseCSS.Bytes(filename) + file, err = m.assets.css.Bytes(filename) case strings.HasPrefix(filename, "/js"): filename = strings.Replace(filename, "/js/", "", 1) - file, err = m.Assets.requiredJS.Bytes(filename) - case strings.HasPrefix(filename, "/vendor"): - if m.Assets.JS != nil { - filename = strings.Replace(filename, "/vendor/", "", 1) - file, err = m.Assets.JS.Bytes(filename) - break - } - - fallthrough + file, err = m.assets.js.Bytes(filename) default: err = errors.New("not found") } diff --git a/http_checksum.go b/checksum.go similarity index 100% rename from http_checksum.go rename to checksum.go diff --git a/http_command.go b/command.go similarity index 100% rename from http_command.go rename to command.go diff --git a/http_download.go b/download.go similarity index 100% rename from http_download.go rename to download.go diff --git a/editor.go b/editor.go index 3ffcf872..b574dea9 100644 --- a/editor.go +++ b/editor.go @@ -87,6 +87,50 @@ Error: return e, nil } +// serveSingle serves a single file in an editor (if it is editable), shows the +// plain file, or downloads it if it can't be shown. +func serveSingle(w http.ResponseWriter, r *http.Request, c *FileManager, u *User, i *fileInfo) (int, error) { + var err error + + if err = i.RetrieveFileType(); err != nil { + return errorToHTTP(err, true), err + } + + p := &page{ + Name: i.Name, + Path: i.VirtualPath, + IsDir: false, + Data: i, + User: u, + PrefixURL: c.PrefixURL, + BaseURL: c.AbsoluteURL(), + WebDavURL: c.AbsoluteWebDavURL(), + } + + // If the request accepts JSON, we send the file information. + if strings.Contains(r.Header.Get("Accept"), "application/json") { + return p.PrintAsJSON(w) + } + + if i.Type == "text" { + if err = i.Read(); err != nil { + return errorToHTTP(err, true), err + } + } + + if i.CanBeEdited() && u.AllowEdit { + p.Data, err = getEditor(r, i) + p.Editor = true + if err != nil { + return http.StatusInternalServerError, err + } + + return p.PrintAsHTML(w, c, "frontmatter", "editor") + } + + return p.PrintAsHTML(w, c, "single") +} + func editorClass(mode string) string { switch mode { case "json", "toml", "yaml": diff --git a/filemanager.go b/filemanager.go index 12599057..9ec2bcc6 100644 --- a/filemanager.go +++ b/filemanager.go @@ -19,7 +19,7 @@ var ( // 'New' function and not directly. type FileManager struct { *User - Assets *assets + assets *assets // BaseURL is the path where the GUI will be accessible. It musn't end with // a trailing slash and mustn't contain PrefixURL, if set. Despite being @@ -79,12 +79,9 @@ type User struct { // assets are the static and front-end assets, such as JS, CSS and HTML templates. type assets struct { - requiredJS *rice.Box // JS that is always required to have in order to be usable. - baseTemplates *rice.Box - baseCSS *rice.Box - Templates *rice.Box - CSS *rice.Box - JS *rice.Box + templates *rice.Box + css *rice.Box + js *rice.Box } // Rule is a dissalow/allow rule. @@ -116,10 +113,10 @@ func New(scope string) *FileManager { Users: map[string]*User{}, BeforeSave: func(r *http.Request, m *FileManager, u *User) error { return nil }, AfterSave: func(r *http.Request, m *FileManager, u *User) error { return nil }, - Assets: &assets{ - baseTemplates: rice.MustFindBox("./_assets/templates"), - baseCSS: rice.MustFindBox("./_assets/css"), - requiredJS: rice.MustFindBox("./_assets/js"), + assets: &assets{ + templates: rice.MustFindBox("./_assets/templates"), + css: rice.MustFindBox("./_assets/css"), + js: rice.MustFindBox("./_assets/js"), }, } @@ -225,6 +222,105 @@ func (m *FileManager) NewUser(username string) error { return nil } +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + var ( + u *User + code int + err error + ) + + // Checks if the URL matches the Assets URL. Returns the asset if the + // method is GET and Status Forbidden otherwise. + if matchURL(r.URL.Path, m.BaseURL+assetsURL) { + if r.Method == http.MethodGet { + return serveAssets(w, r, m) + } + + return http.StatusForbidden, nil + } + + username, _, _ := r.BasicAuth() + if _, ok := m.Users[username]; ok { + u = m.Users[username] + } else { + u = m.User + } + + // Checks if the request URL is for the WebDav server + if matchURL(r.URL.Path, m.WebDavURL) { + return serveWebDAV(w, r, m, u) + } + + 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 !u.Allowed(strings.TrimPrefix(r.URL.Path, m.BaseURL)) { + if r.Method == http.MethodGet { + return htmlError( + w, http.StatusForbidden, + errors.New("You don't have permission to access this page"), + ) + } + + return http.StatusForbidden, nil + } + + if r.URL.Query().Get("search") != "" { + return search(w, r, m, u) + } + + if r.URL.Query().Get("command") != "" { + return command(w, r, m, u) + } + + if r.Method == http.MethodGet { + var f *fileInfo + + // Obtains the information of the directory/file. + f, err = getInfo(r.URL, m, u) + if err != nil { + if r.Method == http.MethodGet { + return htmlError(w, code, err) + } + + code = errorToHTTP(err, false) + return code, err + } + + // If it's a dir and the path doesn't end with a trailing slash, + // redirect the user. + if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") { + http.Redirect(w, r, m.PrefixURL+r.URL.Path+"/", http.StatusTemporaryRedirect) + return 0, nil + } + + switch { + case r.URL.Query().Get("download") != "": + code, err = download(w, r, f) + case !f.IsDir && r.URL.Query().Get("checksum") != "": + code, err = checksum(w, r, f) + case r.URL.Query().Get("raw") == "true" && !f.IsDir: + http.ServeFile(w, r, f.Path) + code, err = 0, nil + case f.IsDir: + code, err = serveListing(w, r, m, u, f) + default: + code, err = serveSingle(w, r, m, u, f) + } + + if err != nil { + code, err = htmlError(w, code, err) + } + + return code, err + } + + return http.StatusNotImplemented, nil +} + // Allowed checks if the user has permission to access a directory/file. func (u User) Allowed(url string) bool { var rule *Rule diff --git a/http.go b/http.go index 542a705d..6eca02ee 100644 --- a/http.go +++ b/http.go @@ -1,13 +1,38 @@ package filemanager import ( - "errors" "net/http" "os" - "path/filepath" "strings" ) +// 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) +} + +// matchURL checks if the first URL matches the second. func matchURL(first, second string) bool { first = strings.ToLower(first) second = strings.ToLower(second) @@ -15,167 +40,20 @@ func matchURL(first, second string) bool { return strings.HasPrefix(first, second) } -// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. -func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - var ( - u *User - code int - err error - ) - - // Checks if the URL matches the Assets URL. Returns the asset if the - // method is GET and Status Forbidden otherwise. - if matchURL(r.URL.Path, m.BaseURL+assetsURL) { - if r.Method == http.MethodGet { - return serveAssets(w, r, m) +// errorToHTTP converts errors to HTTP Status Code. +func errorToHTTP(err error, gone bool) int { + switch { + case os.IsPermission(err): + return http.StatusForbidden + case os.IsNotExist(err): + if !gone { + return http.StatusNotFound } - return http.StatusForbidden, nil + return http.StatusGone + case os.IsExist(err): + return http.StatusGone + default: + return http.StatusInternalServerError } - - username, _, _ := r.BasicAuth() - if _, ok := m.Users[username]; ok { - u = m.Users[username] - } else { - u = m.User - } - - // Checks if the request URL is for the WebDav server - if matchURL(r.URL.Path, m.WebDavURL) { - return serveWebDAV(w, r, m, u) - } - - 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 !u.Allowed(strings.TrimPrefix(r.URL.Path, m.BaseURL)) { - if r.Method == http.MethodGet { - return htmlError( - w, http.StatusForbidden, - errors.New("You don't have permission to access this page"), - ) - } - - return http.StatusForbidden, nil - } - - if r.URL.Query().Get("search") != "" { - return search(w, r, m, u) - } - - if r.URL.Query().Get("command") != "" { - return command(w, r, m, u) - } - - if r.Method == http.MethodGet { - var f *fileInfo - - // Obtains the information of the directory/file. - f, err = getInfo(r.URL, m, u) - if err != nil { - if r.Method == http.MethodGet { - return htmlError(w, code, err) - } - - code = errorToHTTP(err, false) - return code, err - } - - // If it's a dir and the path doesn't end with a trailing slash, - // redirect the user. - if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, m.PrefixURL+r.URL.Path+"/", http.StatusTemporaryRedirect) - return 0, nil - } - - switch { - case r.URL.Query().Get("download") != "": - code, err = download(w, r, f) - case !f.IsDir && r.URL.Query().Get("checksum") != "": - code, err = checksum(w, r, f) - case r.URL.Query().Get("raw") == "true" && !f.IsDir: - http.ServeFile(w, r, f.Path) - code, err = 0, nil - case f.IsDir: - code, err = serveListing(w, r, m, u, f) - default: - code, err = serveSingle(w, r, m, u, f) - } - - if err != nil { - code, err = htmlError(w, code, err) - } - - return code, err - } - - return http.StatusNotImplemented, nil -} - -// serveWebDAV handles the webDAV route of the File Manager. -func serveWebDAV(w http.ResponseWriter, r *http.Request, m *FileManager, u *User) (int, error) { - var err error - - // Checks for user permissions relatively to this path. - if !u.Allowed(strings.TrimPrefix(r.URL.Path, m.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, m.WebDavURL, "", 1) - path = u.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 = newResponseWriterNoBody(w) - } - } - case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE": - if !u.AllowEdit { - return http.StatusForbidden, nil - } - case "MKCOL", "COPY": - if !u.AllowNew { - return http.StatusForbidden, nil - } - } - - // Preprocess the PUT request if it's the case - if r.Method == http.MethodPut { - if err = m.BeforeSave(r, m, u); err != nil { - return http.StatusInternalServerError, err - } - - if put(w, r, m, u) != nil { - return http.StatusInternalServerError, err - } - } - - m.handler.ServeHTTP(w, r) - if err = m.AfterSave(r, m, u); err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil } diff --git a/http_single.go b/http_single.go index 7d17f7d6..2e5b8ff3 100644 --- a/http_single.go +++ b/http_single.go @@ -1,50 +1 @@ package filemanager - -import ( - "net/http" - "strings" -) - -// serveSingle serves a single file in an editor (if it is editable), shows the -// plain file, or downloads it if it can't be shown. -func serveSingle(w http.ResponseWriter, r *http.Request, c *FileManager, u *User, i *fileInfo) (int, error) { - var err error - - if err = i.RetrieveFileType(); err != nil { - return errorToHTTP(err, true), err - } - - p := &page{ - Name: i.Name, - Path: i.VirtualPath, - IsDir: false, - Data: i, - User: u, - PrefixURL: c.PrefixURL, - BaseURL: c.AbsoluteURL(), - WebDavURL: c.AbsoluteWebDavURL(), - } - - // If the request accepts JSON, we send the file information. - if strings.Contains(r.Header.Get("Accept"), "application/json") { - return p.PrintAsJSON(w) - } - - if i.Type == "text" { - if err = i.Read(); err != nil { - return errorToHTTP(err, true), err - } - } - - if i.CanBeEdited() && u.AllowEdit { - p.Data, err = getEditor(r, i) - p.Editor = true - if err != nil { - return http.StatusInternalServerError, err - } - - return p.PrintAsHTML(w, c, "frontmatter", "editor") - } - - return p.PrintAsHTML(w, c, "single") -} diff --git a/http_utils.go b/http_utils.go deleted file mode 100644 index cbb9fa3a..00000000 --- a/http_utils.go +++ /dev/null @@ -1,50 +0,0 @@ -package filemanager - -import ( - "net/http" - "os" -) - -// 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) -} - -// errorToHTTP converts errors to HTTP Status Code. -func errorToHTTP(err error, gone bool) int { - switch { - case os.IsPermission(err): - return http.StatusForbidden - case os.IsNotExist(err): - if !gone { - return http.StatusNotFound - } - - return http.StatusGone - case os.IsExist(err): - return http.StatusGone - default: - return http.StatusInternalServerError - } -} diff --git a/page.go b/page.go index 9324718d..22a0beae 100644 --- a/page.go +++ b/page.go @@ -117,7 +117,7 @@ func (p page) PrintAsHTML(w http.ResponseWriter, m *FileManager, templates ...st // For each template, add it to the the tpl variable for i, t := range templates { // Get the template from the assets - Page, err := getTemplate(m, t+".tmpl") + Page, err := m.assets.templates.String(t + ".tmpl") // Check if there is some error. If so, the template doesn't exist if err != nil { @@ -165,13 +165,3 @@ func (p page) PrintAsJSON(w http.ResponseWriter) (int, error) { return http.StatusOK, nil } - -func getTemplate(m *FileManager, template string) (string, error) { - if m.Assets.Templates != nil { - if tpl, err := m.Assets.Templates.String(template); err == nil { - return tpl, err - } - } - - return m.Assets.baseTemplates.String(template) -} diff --git a/http_search.go b/search.go similarity index 100% rename from http_search.go rename to search.go diff --git a/webdav.go b/webdav.go new file mode 100644 index 00000000..9a041ee5 --- /dev/null +++ b/webdav.go @@ -0,0 +1,74 @@ +package filemanager + +import ( + "net/http" + "os" + "path/filepath" + "strings" +) + +// serveWebDAV handles the webDAV route of the File Manager. +func serveWebDAV(w http.ResponseWriter, r *http.Request, m *FileManager, u *User) (int, error) { + var err error + + // Checks for user permissions relatively to this path. + if !u.Allowed(strings.TrimPrefix(r.URL.Path, m.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, m.WebDavURL, "", 1) + path = u.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 = newResponseWriterNoBody(w) + } + } + case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE": + if !u.AllowEdit { + return http.StatusForbidden, nil + } + case "MKCOL", "COPY": + if !u.AllowNew { + return http.StatusForbidden, nil + } + } + + // Preprocess the PUT request if it's the case + if r.Method == http.MethodPut { + if err = m.BeforeSave(r, m, u); err != nil { + return http.StatusInternalServerError, err + } + + if put(w, r, m, u) != nil { + return http.StatusInternalServerError, err + } + } + + m.handler.ServeHTTP(w, r) + if err = m.AfterSave(r, m, u); err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +}