diff --git a/frontend b/frontend index 44658d2a..9e6f2651 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit 44658d2a43fefebde554b4adeddfc6ae7e996373 +Subproject commit 9e6f265152bc184f1404d8a62d90deb41fd87ce1 diff --git a/http/http.go b/http/http.go index 084cba33..6a5d64b2 100644 --- a/http/http.go +++ b/http/http.go @@ -6,7 +6,9 @@ import ( "net/http" "strconv" "sync" + "time" + "github.com/gorilla/websocket" "github.com/filebrowser/filebrowser/types" "github.com/gorilla/mux" ) @@ -65,7 +67,7 @@ func Handler(e *Env) http.Handler { api.HandleFunc("/settings", e.auth(e.settingsPutHandler)).Methods("PUT") api.PathPrefix("/raw").HandlerFunc(e.auth(e.rawHandler)).Methods("GET") - api.PathPrefix("/commands").HandlerFunc(e.auth(e.commandsHandler)) + api.PathPrefix("/command").HandlerFunc(e.auth(e.commandsHandler)) api.PathPrefix("/search").HandlerFunc(e.auth(e.searchHandler)) return r @@ -79,6 +81,14 @@ func httpErr(w http.ResponseWriter, r *http.Request, status int, err error) { http.Error(w, strconv.Itoa(status)+" "+txt, status) } +func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) { + txt := http.StatusText(status) + if err != nil || status >= 400 { + log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err) + } + ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(10*time.Second)) +} + func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) { marsh, err := json.Marshal(data) if err != nil { diff --git a/http/websockets.go b/http/websockets.go index 817604fb..7bce98f2 100644 --- a/http/websockets.go +++ b/http/websockets.go @@ -1,12 +1,16 @@ package http import ( + "bufio" "encoding/json" + "io" "net/http" "os" + "os/exec" "strings" "github.com/filebrowser/filebrowser/search" + "github.com/spf13/afero" "github.com/gorilla/websocket" ) @@ -16,14 +20,13 @@ var upgrader = websocket.Upgrader{ WriteBufferSize: 1024, } -/* var ( cmdNotImplemented = []byte("Command not implemented.") cmdNotAllowed = []byte("Command not allowed.") -) */ +) func (e *Env) commandsHandler(w http.ResponseWriter, r *http.Request) { - /* user, ok := e.getUser(w, r) + user, ok := e.getUser(w, r) if !ok { return } @@ -35,114 +38,68 @@ func (e *Env) commandsHandler(w http.ResponseWriter, r *http.Request) { } defer conn.Close() - var ( - message []byte - command []string - ) + var command []string - // Starts an infinite loop until a valid command is captured. for { - _, message, err = conn.ReadMessage() + _, msg, err := conn.ReadMessage() if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) + wsErr(conn, r, http.StatusInternalServerError, err) return } - command = strings.Split(string(message), " ") + command = strings.Split(string(msg), " ") if len(command) != 0 { break } } - allowed := false - - for _, cmd := range user.Commands { - if regexp.MustCompile(cmd).MatchString(command[0]) { - allowed = true - break - } - } - - if !allowed { - err = conn.WriteMessage(websocket.TextMessage, cmdNotAllowed) + if !user.CanExecute(command[0]) { + err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed) if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return + wsErr(conn, r, http.StatusInternalServerError, err) } return } - // Check if the program is installed on the computer. - if _, err = exec.LookPath(command[0]); err != nil { - err = conn.WriteMessage(websocket.TextMessage, cmdNotImplemented) + if _, err := exec.LookPath(command[0]); err != nil { + err := conn.WriteMessage(websocket.TextMessage, cmdNotImplemented) if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } else { - httpErr + wsErr(conn, r, http.StatusInternalServerError, err) } - return http.StatusNotImplemented, nil + return } - // Gets the path and initializes a buffer. - path := c.User.Scope + "/" + r.URL.Path - path = filepath.Clean(path) - buff := new(bytes.Buffer) - - // Sets up the command executation. + path := strings.TrimPrefix(r.URL.Path, "/api/command") + dir := afero.FullBaseFsPath(user.Fs.(*afero.BasePathFs), path) cmd := exec.Command(command[0], command[1:]...) - cmd.Dir = path - cmd.Stderr = buff - cmd.Stdout = buff + cmd.Dir = dir - // Starts the command and checks for fb.Errors. - err = cmd.Start() + stdout, err := cmd.StdoutPipe() if err != nil { - return http.StatusInternalServerError, err + wsErr(conn, r, http.StatusInternalServerError, err) + return + } + stderr, err := cmd.StderrPipe() + if err != nil { + wsErr(conn, r, http.StatusInternalServerError, err) + return } - // Set a 'done' variable to check whetever the command has already finished - // running or not. This verification is done using a goroutine that uses the - // method .Wait() from the command. - done := false - go func() { - err = cmd.Wait() - done = true - }() - - // Function to print the current information on the buffer to the connection. - print := func() error { - by := buff.Bytes() - if len(by) > 0 { - err = conn.WriteMessage(websocket.TextMessage, by) - if err != nil { - return err - } - } - - return nil + if err := cmd.Start(); err != nil { + wsErr(conn, r, http.StatusInternalServerError, err) + return } - // While the command hasn't finished running, continue sending the output - // to the client in intervals of 100 milliseconds. - for !done { - if err = print(); err != nil { - return http.StatusInternalServerError, err - } - - time.Sleep(100 * time.Millisecond) + s := bufio.NewScanner(io.MultiReader(stdout, stderr)) + for s.Scan() { + conn.WriteMessage(websocket.TextMessage, s.Bytes()) } - // After the command is done executing, send the output one more time to the - // browser to make sure it gets the latest information. - if err = print(); err != nil { - return http.StatusInternalServerError, err + if err := cmd.Wait(); err != nil { + wsErr(conn, r, http.StatusInternalServerError, err) } - - return 0, nil */ - } func (e *Env) searchHandler(w http.ResponseWriter, r *http.Request) { diff --git a/types/user.go b/types/user.go index 14e6f25d..b822fc52 100644 --- a/types/user.go +++ b/types/user.go @@ -2,6 +2,7 @@ package types import ( "path/filepath" + "regexp" "github.com/spf13/afero" "golang.org/x/crypto/bcrypt" @@ -103,6 +104,21 @@ func (u *User) IsAllowed(url string) bool { return isAllowed(url, u.Rules) } +// CanExecute checks if an user can execute a specific command. +func (u *User) CanExecute(command string) bool { + if !u.Perm.Execute { + return false + } + + for _, cmd := range u.Commands { + if regexp.MustCompile(cmd).MatchString(command) { + return true + } + } + + return false +} + // ApplyDefaults applies defaults to a user. func (u *User) ApplyDefaults(defaults UserDefaults) { u.Scope = defaults.Scope