From f247a5560fd599db352b187b7ba25ade64690cf0 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 3 Jul 2017 08:59:49 +0100 Subject: [PATCH] Updates on auth and db Former-commit-id: cdcfc50a4a7274482826520644019d54c37a7753 [formerly 376d2422063755c75d5b5c8e21bf8617ca3a9baf] [formerly ea860508c1603296e0439a583ca0573609be3511 [formerly 6312c60b2e99446d0934be5c92a7929efff93a08]] Former-commit-id: d64b8b836568b150853288a69f870662e92db1e0 [formerly b85b1a09bbf87c1b5f2af07659121ef51a74f05e] Former-commit-id: ee164251a6143567da3f970e347ede0d6de1d350 --- _assets/package.json | 1 + _assets/src/utils/cookie.js | 4 + api.go | 27 ++-- auth.go | 125 +++++++++++++++--- caddy/filemanager/filemanager.go | 192 +++++++--------------------- command.go | 2 +- file.go | 6 +- filemanager.go | 212 ++++++++++++++++++------------- http.go | 4 +- search.go | 2 +- 10 files changed, 303 insertions(+), 272 deletions(-) create mode 100644 _assets/src/utils/cookie.js diff --git a/_assets/package.json b/_assets/package.json index 0ac07c70..5a8b7267 100644 --- a/_assets/package.json +++ b/_assets/package.json @@ -13,6 +13,7 @@ "filesize": "^3.5.10", "moment": "^2.18.1", "vue": "^2.3.3", + "vue-router": "^2.7.0", "vuex": "^2.3.1" }, "devDependencies": { diff --git a/_assets/src/utils/cookie.js b/_assets/src/utils/cookie.js new file mode 100644 index 00000000..5004b602 --- /dev/null +++ b/_assets/src/utils/cookie.js @@ -0,0 +1,4 @@ +export default function (name) { + let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$') + return document.cookie.replace(re, '$1') +} diff --git a/api.go b/api.go index 403b3654..d26a90c0 100644 --- a/api.go +++ b/api.go @@ -25,20 +25,19 @@ func cleanURL(path string) (string, string) { } func serveAPI(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { - if r.URL.Path == "/auth" { - return getTokenHandler(c, w, r) + if r.URL.Path == "/auth/get" { + return authHandler(c, w, r) } - /* valid, user := validAuth(c, r) + if r.URL.Path == "/auth/renew" { + return renewAuthHandler(c, w, r) + } + + valid, _ := validateAuth(c, r) if !valid { return http.StatusForbidden, nil } - fmt.Println(user) - c.us = user */ - - c.us = c.fm.User - var router string router, r.URL.Path = cleanURL(r.URL.Path) @@ -175,7 +174,7 @@ func deleteHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (i } // Remove the file or folder. - err := c.us.fileSystem.RemoveAll(context.TODO(), r.URL.Path) + err := c.us.FileSystem.RemoveAll(context.TODO(), r.URL.Path) if err != nil { return errorToHTTP(err, true), err } @@ -185,11 +184,11 @@ func deleteHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (i func putHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { if strings.HasSuffix(r.URL.Path, "/") { - err := c.us.fileSystem.Mkdir(context.TODO(), r.URL.Path, 0666) + err := c.us.FileSystem.Mkdir(context.TODO(), r.URL.Path, 0666) return errorToHTTP(err, false), err } - f, err := c.us.fileSystem.OpenFile(context.TODO(), r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + f, err := c.us.FileSystem.OpenFile(context.TODO(), r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) defer f.Close() if err != nil { @@ -214,8 +213,12 @@ func putHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, func postHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { dst := r.Header.Get("Destination") src := r.URL.Path - err := c.us.fileSystem.Rename(context.TODO(), src, dst) + if dst == "/" || src == "/" { + return http.StatusForbidden, nil + } + + err := c.us.FileSystem.Rename(context.TODO(), src, dst) return errorToHTTP(err, true), err } diff --git a/auth.go b/auth.go index 85a2c703..8ce07af7 100644 --- a/auth.go +++ b/auth.go @@ -1,36 +1,56 @@ package filemanager import ( + "encoding/json" + "math/rand" "net/http" "time" + "golang.org/x/crypto/bcrypt" + jwt "github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go/request" ) -/* Set up a global string for our secret */ -var key = []byte("secret") - type claims struct { *User jwt.StandardClaims } -func getTokenHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { - // TODO: get user and password info from the request - // check if the password is correct for that user using a DB or JSOn - // or something. +// authHandler proccesses the authentication for the user. +func authHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { + // Receive the credentials from the request and unmarshal them. + var cred User + if r.Body == nil { + return http.StatusForbidden, nil + } + + err := json.NewDecoder(r.Body).Decode(&cred) + if err != nil { + return http.StatusForbidden, nil + } + + // Checks if the user exists. + u, ok := c.fm.Users[cred.Username] + if !ok { + return http.StatusForbidden, nil + } + + // Checks if the password is correct. + if !checkPasswordHash(cred.Password, u.Password) { + return http.StatusForbidden, nil + } claims := claims{ - c.fm.User, + c.fm.Users["admin"], jwt.StandardClaims{ - ExpiresAt: time.Now().Add(time.Minute * 5).Unix(), - Issuer: "test", + ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), + Issuer: "File Manager", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - string, err := token.SignedString(key) + string, err := token.SignedString(c.fm.key) if err != nil { return http.StatusInternalServerError, err @@ -40,15 +60,80 @@ func getTokenHandler(c *requestContext, w http.ResponseWriter, r *http.Request) return 0, nil } -func validAuth(c *requestContext, r *http.Request) (bool, *User) { - token, err := request.ParseFromRequestWithClaims(r, request.AuthorizationHeaderExtractor, &claims{}, - func(token *jwt.Token) (interface{}, error) { - return key, nil - }) - - if err == nil && token.Valid { - return true, c.fm.User +// renewAuthHandler is used when the front-end already has a JWT token +// and is checking if it is up to date. If so, updates its info. +func renewAuthHandler(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { + ok, u := validateAuth(c, r) + if !ok { + return http.StatusForbidden, nil } - return false, nil + claims := claims{ + u, + jwt.StandardClaims{ + ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), + Issuer: "File Manager", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + string, err := token.SignedString(c.fm.key) + if err != nil { + return http.StatusInternalServerError, err + } + + w.Write([]byte(string)) + return 0, nil +} + +// validateAuth is used to validate the authentication and returns the +// User if it is valid. +func validateAuth(c *requestContext, r *http.Request) (bool, *User) { + keyFunc := func(token *jwt.Token) (interface{}, error) { + return c.fm.key, nil + } + + var claims claims + token, err := request.ParseFromRequestWithClaims(r, + request.AuthorizationHeaderExtractor, + &claims, + keyFunc, + ) + + if err != nil || !token.Valid { + return false, nil + } + + u, ok := c.fm.Users[claims.User.Username] + if !ok { + return false, nil + } + + c.us = u + return true, u +} + +// hashPassword generates an hash from a password using bcrypt. +func hashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// checkPasswordHash compares a password with an hash to check if they match. +func checkPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +// randomString creates a string with a defined length using the above charset. +func randomString(length int) string { + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) } diff --git a/caddy/filemanager/filemanager.go b/caddy/filemanager/filemanager.go index 68af4fc0..81e6d597 100644 --- a/caddy/filemanager/filemanager.go +++ b/caddy/filemanager/filemanager.go @@ -4,17 +4,15 @@ package filemanager import ( - "fmt" - "io/ioutil" "log" "net/http" "os" "os/exec" "path/filepath" - "regexp" - "strconv" "strings" + "golang.org/x/net/webdav" + . "github.com/hacdias/filemanager" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" @@ -34,8 +32,7 @@ type plugin struct { type config struct { *FileManager - baseURL string - webDavURL string + baseURL string } // ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. @@ -69,152 +66,55 @@ func setup(c *caddy.Controller) error { func parse(c *caddy.Controller) ([]*config, error) { var ( configs []*config - err error ) for c.Next() { - var ( - m = &config{FileManager: New(".")} - u = m.User - name = "" - ) + // TODO: + // filemanager [baseurl] [baseScope] { + // database path + // } + + baseURL := "/" + baseScope := "." + + // Get the baseURL and baseScope + args := c.RemainingArgs() + + if len(args) == 1 { + baseURL = args[0] + } + + if len(args) > 1 { + baseScope = args[1] + } + + fm, err := New("./this.db", User{ + Username: "admin", + Password: "admin", + AllowCommands: true, + AllowEdit: true, + AllowNew: true, + Commands: []string{"git", "svn", "hg"}, + Rules: []*Rule{{ + Regex: true, + Allow: false, + Regexp: &Regexp{Raw: "\\/\\..+"}, + }}, + CSS: "", + FileSystem: webdav.Dir(baseScope), + }) + + if err != nil { + return nil, err + } caddyConf := httpserver.GetConfig(c) + m := &config{FileManager: fm} + m.SetBaseURL(baseURL) m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/")) - m.Commands = []string{"git", "svn", "hg"} - m.Rules = append(m.Rules, &Rule{ - Regex: true, - Allow: false, - Regexp: regexp.MustCompile("\\/\\..+"), - }) + m.baseURL = strings.TrimSuffix(baseURL, "/") - // Get the baseURL - args := c.RemainingArgs() - - if len(args) > 0 { - m.baseURL = args[0] - m.SetBaseURL(args[0]) - } - - for c.NextBlock() { - switch c.Val() { - case "before_save": - if m.BeforeSave, err = makeCommand(c, m); err != nil { - return configs, err - } - case "after_save": - if m.AfterSave, err = makeCommand(c, m); err != nil { - return configs, err - } - 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 := &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 - } - } - - m.baseURL = strings.TrimSuffix(m.baseURL, "/") - m.webDavURL = strings.TrimSuffix(m.webDavURL, "/") configs = append(configs, m) } @@ -242,8 +142,8 @@ func makeCommand(c *caddy.Controller, m *config) (Command, error) { } fn = func(r *http.Request, c *FileManager, u *User) error { - path := strings.Replace(r.URL.Path, m.baseURL+m.webDavURL, "", 1) - path = u.Scope + "/" + path + path := strings.Replace(r.URL.Path, m.baseURL+"/files", "", 1) + path = string(u.FileSystem) + "/" + path path = filepath.Clean(path) for i := range args { diff --git a/command.go b/command.go index ae24bfe0..bc22e1c4 100644 --- a/command.go +++ b/command.go @@ -77,7 +77,7 @@ func command(c *requestContext, w http.ResponseWriter, r *http.Request) (int, er } // Gets the path and initializes a buffer. - path := c.us.Scope + "/" + r.URL.Path + path := string(c.us.FileSystem) + "/" + r.URL.Path path = filepath.Clean(path) buff := new(bytes.Buffer) diff --git a/file.go b/file.go index ed5a11b5..5fa4e353 100644 --- a/file.go +++ b/file.go @@ -85,10 +85,10 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) { i := &file{ URL: c.RootURL() + "/files" + url.Path, VirtualPath: url.Path, - Path: filepath.Join(u.Scope, url.Path), + Path: filepath.Join(string(u.FileSystem), url.Path), } - info, err := u.fileSystem.Stat(context.TODO(), url.Path) + info, err := u.FileSystem.Stat(context.TODO(), url.Path) if err != nil { return i, err } @@ -106,7 +106,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) { func (i *file) getListing(c *requestContext, r *http.Request) error { // Gets the directory information using the Virtual File System of // the user configuration. - f, err := c.us.fileSystem.OpenFile(context.TODO(), c.fi.VirtualPath, os.O_RDONLY, 0) + f, err := c.us.FileSystem.OpenFile(context.TODO(), c.fi.VirtualPath, os.O_RDONLY, 0) if err != nil { return err } diff --git a/filemanager.go b/filemanager.go index 1fb527f0..8d53e6a7 100644 --- a/filemanager.go +++ b/filemanager.go @@ -7,6 +7,7 @@ import ( "strings" rice "github.com/GeertJohan/go.rice" + "github.com/asdine/storm" "golang.org/x/net/webdav" ) @@ -18,26 +19,22 @@ var ( // FileManager is a file manager instance. It should be creating using the // 'New' function and not directly. type FileManager struct { - *User + db *storm.DB + key []byte - // prefixURL is a part of the URL that is already trimmed from the request URL before it + // PrefixURL is a part of the URL that is already trimmed from the request URL before it // arrives to our handlers. It may be useful when using File Manager as a middleware // such as in caddy-filemanager plugin. It is only useful in certain situations. - prefixURL string + PrefixURL string - // 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. - baseURL string + // 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. It shouldn't be + // edited directly. Use SetBaseURL. + BaseURL string // Users is a map with the different configurations for each user. Users map[string]*User - // BeforeSave is a function that is called before saving a file. - BeforeSave Command - - // AfterSave is a function that is called before saving a file. - AfterSave Command - assets *rice.Box } @@ -47,17 +44,24 @@ type Command func(r *http.Request, m *FileManager, u *User) error // User contains the configuration for each user. It should be created // using NewUser on a File Manager instance. type User struct { - // Scope is the physical path the user has access to. - Scope string + // ID is the required primary key with auto increment0 + ID int `storm:"id,increment"` - // fileSystem is the virtual file system the user has access. - fileSystem webdav.FileSystem + // Username is the user username used to login. + Username string `json:"username" storm:"index,unique"` + + // The hashed password. This never reaches the front-end because it's temporarily + // emptied during JSON marshall. + Password string `json:"password"` + + // FileSystem is the virtual file system the user has access. + FileSystem webdav.Dir `json:"filesystem"` // Rules is an array of access and deny rules. - Rules []*Rule `json:"-"` + Rules []*Rule `json:"rules"` - // TODO: this MUST be done in another way - StyleSheet string `json:"-"` + // Costum styles for this user. + CSS string `json:"css"` // These indicate if the user can perform certain actions. AllowNew bool `json:"allowNew"` // Create files and folders @@ -80,42 +84,101 @@ type Rule struct { Path string // Regexp is the regular expression. Only use this when 'Regex' was set to true. - Regexp *regexp.Regexp + Regexp *Regexp } -// New creates a new File Manager instance with the needed -// configuration to work. -func New(scope string) *FileManager { +// Regexp is a regular expression wrapper around native regexp. +type Regexp struct { + Raw string + regexp *regexp.Regexp +} + +// DefaultUser is used on New, when no 'base' user is provided. +var DefaultUser = User{ + Username: "admin", + Password: "admin", + AllowCommands: true, + AllowEdit: true, + AllowNew: true, + Commands: []string{}, + Rules: []*Rule{}, + CSS: "", + FileSystem: webdav.Dir("."), +} + +// New creates a new File Manager instance. If 'database' file already +// exists, it will load the users from there. Otherwise, a new user +// will be created using the 'base' variable. The 'base' User should +// not have the Password field hashed. +// TODO: should it ask for a baseURL on New???? +func New(database string, base User) (*FileManager, error) { + // Creates a new File Manager instance with the Users + // map and Assets box. m := &FileManager{ - User: &User{ - AllowCommands: true, - AllowEdit: true, - AllowNew: true, - Commands: []string{}, - Rules: []*Rule{}, - }, - 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: rice.MustFindBox("./_assets/dist"), + Users: map[string]*User{}, + assets: rice.MustFindBox("./_assets/dist"), } - m.SetScope(scope, "") - m.SetBaseURL("/") + // Tries to open a database on the location provided. This + // function will automatically create a new one if it doesn't + // exist. + db, err := storm.Open(database) + if err != nil { + return nil, err + } - return m + // Tries to get the encryption key from the database. + // If it doesn't exist, create a new one of 256 bits. + err = db.Get("config", "key", &m.key) + if err != nil && err == storm.ErrNotFound { + m.key = []byte(randomString(64)) + err = db.Set("config", "key", m.key) + } + + if err != nil { + return nil, err + } + + // Tries to fetch the users from the database and if there are + // any, add them to the current File Manager instance. + var users []User + err = db.All(&users) + if err != nil { + return nil, err + } + + for i := range users { + m.Users[users[i].Username] = &users[i] + } + + // If there are no users in the database, it creates a new one + // based on 'base' User that must be provided by the function caller. + if len(users) == 0 { + // Hashes the password. + pw, err := hashPassword(base.Password) + if err != nil { + return nil, err + } + + base.Password = pw + + // Saves the user to the database. + if err := db.Save(&base); err != nil { + return nil, err + } + + m.Users[base.Username] = &base + } + + // Attaches db to this File Manager instance. + m.db = db + return m, nil } // RootURL returns the actual URL where // File Manager interface can be accessed. func (m FileManager) RootURL() string { - return m.prefixURL + m.baseURL -} - -// WebDavURL returns the actual URL -// where WebDAV can be accessed. -func (m FileManager) WebDavURL() string { - return m.prefixURL + m.baseURL + "/api/webdav" + return m.PrefixURL + m.BaseURL } // SetPrefixURL updates the prefixURL of a File @@ -124,7 +187,7 @@ func (m *FileManager) SetPrefixURL(url string) { url = strings.TrimPrefix(url, "/") url = strings.TrimSuffix(url, "/") url = "/" + url - m.prefixURL = strings.TrimSuffix(url, "/") + m.PrefixURL = strings.TrimSuffix(url, "/") } // SetBaseURL updates the baseURL of a File Manager @@ -133,48 +196,7 @@ func (m *FileManager) SetBaseURL(url string) { url = strings.TrimPrefix(url, "/") url = strings.TrimSuffix(url, "/") url = "/" + url - m.baseURL = strings.TrimSuffix(url, "/") -} - -// SetScope updates a user scope and its virtual file system. -// If the user string is blank, it will change the base scope. -func (m *FileManager) SetScope(scope string, username string) error { - var u *User - - if username == "" { - u = m.User - } else { - var ok bool - u, ok = m.Users[username] - if !ok { - return errors.New("Inexistent user") - } - } - - u.Scope = strings.TrimSuffix(scope, "/") - u.fileSystem = webdav.Dir(u.Scope) - - return nil -} - -// NewUser creates a new user on a File Manager struct -// which inherits its configuration from the main user. -func (m *FileManager) NewUser(username string) error { - if _, ok := m.Users[username]; ok { - return ErrDuplicated - } - - m.Users[username] = &User{ - fileSystem: m.User.fileSystem, - Scope: m.User.Scope, - Rules: m.User.Rules, - AllowNew: m.User.AllowNew, - AllowEdit: m.User.AllowEdit, - AllowCommands: m.User.AllowCommands, - Commands: m.User.Commands, - } - - return nil + m.BaseURL = strings.TrimSuffix(url, "/") } // ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. @@ -208,3 +230,19 @@ func (u User) Allowed(url string) bool { return true } + +// SetScope updates a user scope and its virtual file system. +// If the user string is blank, it will change the base scope. +func (u *User) SetScope(scope string) { + scope = strings.TrimSuffix(scope, "/") + u.FileSystem = webdav.Dir(scope) +} + +// MatchString checks if this string matches the regular expression. +func (r *Regexp) MatchString(s string) bool { + if r.regexp == nil { + r.regexp = regexp.MustCompile(r.Raw) + } + + return r.regexp.MatchString(s) +} diff --git a/http.go b/http.go index 402a0459..fa209b94 100644 --- a/http.go +++ b/http.go @@ -19,9 +19,9 @@ type requestContext struct { func serveHTTP(c *requestContext, w http.ResponseWriter, r *http.Request) (int, error) { // Checks if the URL contains the baseURL and strips it. Otherwise, it just // returns a 404 error because we're not supposed to be here! - p := strings.TrimPrefix(r.URL.Path, c.fm.baseURL) + p := strings.TrimPrefix(r.URL.Path, c.fm.BaseURL) - if len(p) >= len(r.URL.Path) && c.fm.baseURL != "" { + if len(p) >= len(r.URL.Path) && c.fm.BaseURL != "" { return http.StatusNotFound, nil } diff --git a/search.go b/search.go index 5ef14246..60afd0b2 100644 --- a/search.go +++ b/search.go @@ -73,7 +73,7 @@ func search(c *requestContext, w http.ResponseWriter, r *http.Request) (int, err search = parseSearch(value) scope := strings.TrimPrefix(r.URL.Path, "/") scope = "/" + scope - scope = c.us.Scope + scope + scope = string(c.us.FileSystem) + scope scope = strings.Replace(scope, "\\", "/", -1) scope = filepath.Clean(scope)