From eaf13425594a81844e08589110c9587702014e2a Mon Sep 17 00:00:00 2001 From: Pierre Dubouilh Date: Sat, 28 Sep 2024 18:02:16 +0200 Subject: [PATCH] more conservative text editor --- gossa.go | 52 ++++++++++++++++++++++++++++------------------ test-fixture/b.txt | 3 ++- ui/script.js | 35 ++++++++++++++++++++----------- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/gossa.go b/gossa.go index 25ef2f3..16eaba4 100644 --- a/gossa.go +++ b/gossa.go @@ -45,11 +45,6 @@ var verb = flag.Bool("verb", false, "verbosity") var skipHidden = flag.Bool("k", true, "\nskip hidden files") var ro = flag.Bool("ro", false, "read only mode (no upload, rename, move, etc...)") -type rpcCall struct { - Call string `json:"call"` - Args []string `json:"args"` -} - var rootPath = "" var handler http.Handler @@ -134,7 +129,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { w.Header().Set("Content-Type", "text/html") w.Header().Add("Content-Encoding", "gzip") - gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed) // BestSpeed is Much Faster than default - base on a very unscientific local test, and only ~30% larger (compression remains still very effective, ~6x) + gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed) // BestSpeed is Much Faster than default - base on a very unscientific local test check(err) defer gz.Close() tmpl.Execute(gz, p) @@ -151,7 +146,8 @@ func doContent(w http.ResponseWriter, r *http.Request) { path := html.UnescapeString(r.URL.Path) defer exitPath(w, "get content", path) - fullPath := enforcePath(path) + fullPath, err := enforcePath(path) + check(err) stat, errStat := os.Stat(fullPath) check(errStat) @@ -174,7 +170,9 @@ func upload(w http.ResponseWriter, r *http.Request) { if err != nil && err != io.EOF { // errs EOF when no more parts to process check(err) } - dst, err := os.Create(enforcePath(path)) + path, err = enforcePath(path) + check(err) + dst, err := os.Create(path) check(err) io.Copy(dst, part) w.Write([]byte("ok")) @@ -184,8 +182,9 @@ func zipRPC(w http.ResponseWriter, r *http.Request) { zipPath := r.URL.Query().Get("zipPath") zipName := r.URL.Query().Get("zipName") defer exitPath(w, "zip", zipPath) - zipFullPath := enforcePath(zipPath) - _, err := os.Lstat(zipFullPath) + zipFullPath, err := enforcePath(zipPath) + check(err) + _, err = os.Lstat(zipFullPath) check(err) w.Header().Add("Content-Disposition", "attachment; filename=\""+zipName+".zip\"") zipWriter := zip.NewWriter(w) @@ -203,7 +202,7 @@ func zipRPC(w http.ResponseWriter, r *http.Request) { return nil // hidden files not allowed } if f.Mode()&os.ModeSymlink != 0 { - panic(errors.New("symlink not allowed in zip downloads")) // filepath.Walk doesnt support symlinks + check(errors.New("symlink not allowed in zip downloads")) // filepath.Walk doesnt support symlinks } header, err := zip.FileInfoHeader(f) @@ -224,39 +223,52 @@ func zipRPC(w http.ResponseWriter, r *http.Request) { } func rpc(w http.ResponseWriter, r *http.Request) { - var err error + type rpcCall struct { + Call string `json:"call"` + Args []string `json:"args"` + } var rpc rpcCall defer exitPath(w, "rpc", rpc) bodyBytes, err := io.ReadAll(r.Body) check(err) json.Unmarshal(bodyBytes, &rpc) + path0, err := enforcePath(rpc.Args[0]) + path1 := "" + check(err) + if len(rpc.Args) > 1 { + path1, err = enforcePath(rpc.Args[1]) + check(err) + } + if rpc.Call == "mkdirp" { - err = os.MkdirAll(enforcePath(rpc.Args[0]), os.ModePerm) - } else if rpc.Call == "mv" { - err = os.Rename(enforcePath(rpc.Args[0]), enforcePath(rpc.Args[1])) + err = os.MkdirAll(path0, os.ModePerm) + } else if rpc.Call == "mv" && len(rpc.Args) == 2 { + err = os.Rename(path0, path1) } else if rpc.Call == "rm" { - err = os.RemoveAll(enforcePath(rpc.Args[0])) + err = os.RemoveAll(path0) + } else { + err = errors.New("invalid rpc call") } check(err) w.Write([]byte("ok")) } -func enforcePath(p string) string { +func enforcePath(p string) (string, error) { joined := filepath.Join(rootPath, strings.TrimPrefix(p, *extraPath)) fp, err := filepath.Abs(joined) - sl, _ := filepath.EvalSymlinks(fp) // err skipped as it would error for unexistent files (RPC check). The actual behaviour is tested below + sl, _ := filepath.EvalSymlinks(fp) // err skipped as it would error for inexistent files (RPC check). The actual behaviour is tested below // panic if we had a error getting absolute path, // ... or if path doesnt contain the prefix path we expect, // ... or if we're skipping hidden folders, and one is requested, // ... or if we're skipping symlinks, path exists, and a symlink out of bound requested if err != nil || !strings.HasPrefix(fp, rootPath) || *skipHidden && strings.Contains(p, "/.") || !*symlinks && len(sl) > 0 && !strings.HasPrefix(sl, rootPath) { - panic(errors.New("invalid path")) + return "", errors.New("invalid path") } - return fp + return fp, nil } func main() { diff --git a/test-fixture/b.txt b/test-fixture/b.txt index 5a2e5f3..5ecf24d 100644 --- a/test-fixture/b.txt +++ b/test-fixture/b.txt @@ -1 +1,2 @@ -B!!! +B!!! +test \ No newline at end of file diff --git a/ui/script.js b/ui/script.js index e8f0701..e67a200 100755 --- a/ui/script.js +++ b/ui/script.js @@ -153,8 +153,11 @@ function rpc (call, args, cb) { xhr.open('POST', location.origin + window.extraPath + '/rpc') xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') xhr.send(JSON.stringify({ call, args })) - xhr.onload = cb - xhr.onerror = () => flicker(sadBadge) + xhr.onload = () => cb(false) + xhr.onerror = () => { + flicker(sadBadge) + cb(true) + } } const mkdirCall = (path, cb) => rpc('mkdirp', [prependPath(path)], cb) @@ -315,28 +318,36 @@ const textTypes = ['.txt', '.rtf', '.md', '.markdown', '.log', '.yaml', '.yml'] const isTextFile = src => src && textTypes.find(type => src.toLocaleLowerCase().includes(type)) let fileEdited -function saveText (quitting) { +function saveText (cb) { const formData = new FormData() formData.append(fileEdited, editor.value) - const path = encodeURIComponent(decodeURI(location.pathname) + fileEdited) + const fname = fileEdited + ".swp" + const path = encodeURIComponent(decodeURI(location.pathname) + fname) upload(0, formData, path, () => { toast.style.display = 'none' - if (!quitting) return - clearInterval(window.padTimer) - window.onbeforeunload = null - resetView() - softPrev() - refresh() + cb() }, () => { toast.style.display = 'block' - if (!quitting) return alert('cant save!\r\nleave window open to resume saving\r\nwhen connection back up') }) } function padOff () { if (!isEditorMode()) { return } - saveText(true) + const swapfile = fileEdited + ".swp" + saveText(() => { + mvCall(prependPath(swapfile), prependPath(fileEdited), err => { + if (err) { + alert('cant save!\r\nleave window open to resume saving\r\nwhen connection back up') + return + } + clearInterval(window.padTimer) + window.onbeforeunload = null + resetView() + softPrev() + refresh() + }) + }) return true }