Skip to content

Commit eaf1342

Browse files
committed
more conservative text editor
1 parent 31c8ee5 commit eaf1342

File tree

3 files changed

+57
-33
lines changed

3 files changed

+57
-33
lines changed

gossa.go

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ var verb = flag.Bool("verb", false, "verbosity")
4545
var skipHidden = flag.Bool("k", true, "\nskip hidden files")
4646
var ro = flag.Bool("ro", false, "read only mode (no upload, rename, move, etc...)")
4747

48-
type rpcCall struct {
49-
Call string `json:"call"`
50-
Args []string `json:"args"`
51-
}
52-
5348
var rootPath = ""
5449
var handler http.Handler
5550

@@ -134,7 +129,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
134129
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
135130
w.Header().Set("Content-Type", "text/html")
136131
w.Header().Add("Content-Encoding", "gzip")
137-
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)
132+
gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed) // BestSpeed is Much Faster than default - base on a very unscientific local test
138133
check(err)
139134
defer gz.Close()
140135
tmpl.Execute(gz, p)
@@ -151,7 +146,8 @@ func doContent(w http.ResponseWriter, r *http.Request) {
151146

152147
path := html.UnescapeString(r.URL.Path)
153148
defer exitPath(w, "get content", path)
154-
fullPath := enforcePath(path)
149+
fullPath, err := enforcePath(path)
150+
check(err)
155151
stat, errStat := os.Stat(fullPath)
156152
check(errStat)
157153

@@ -174,7 +170,9 @@ func upload(w http.ResponseWriter, r *http.Request) {
174170
if err != nil && err != io.EOF { // errs EOF when no more parts to process
175171
check(err)
176172
}
177-
dst, err := os.Create(enforcePath(path))
173+
path, err = enforcePath(path)
174+
check(err)
175+
dst, err := os.Create(path)
178176
check(err)
179177
io.Copy(dst, part)
180178
w.Write([]byte("ok"))
@@ -184,8 +182,9 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
184182
zipPath := r.URL.Query().Get("zipPath")
185183
zipName := r.URL.Query().Get("zipName")
186184
defer exitPath(w, "zip", zipPath)
187-
zipFullPath := enforcePath(zipPath)
188-
_, err := os.Lstat(zipFullPath)
185+
zipFullPath, err := enforcePath(zipPath)
186+
check(err)
187+
_, err = os.Lstat(zipFullPath)
189188
check(err)
190189
w.Header().Add("Content-Disposition", "attachment; filename=\""+zipName+".zip\"")
191190
zipWriter := zip.NewWriter(w)
@@ -203,7 +202,7 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
203202
return nil // hidden files not allowed
204203
}
205204
if f.Mode()&os.ModeSymlink != 0 {
206-
panic(errors.New("symlink not allowed in zip downloads")) // filepath.Walk doesnt support symlinks
205+
check(errors.New("symlink not allowed in zip downloads")) // filepath.Walk doesnt support symlinks
207206
}
208207

209208
header, err := zip.FileInfoHeader(f)
@@ -224,39 +223,52 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
224223
}
225224

226225
func rpc(w http.ResponseWriter, r *http.Request) {
227-
var err error
226+
type rpcCall struct {
227+
Call string `json:"call"`
228+
Args []string `json:"args"`
229+
}
228230
var rpc rpcCall
229231
defer exitPath(w, "rpc", rpc)
230232
bodyBytes, err := io.ReadAll(r.Body)
231233
check(err)
232234
json.Unmarshal(bodyBytes, &rpc)
233235

236+
path0, err := enforcePath(rpc.Args[0])
237+
path1 := ""
238+
check(err)
239+
if len(rpc.Args) > 1 {
240+
path1, err = enforcePath(rpc.Args[1])
241+
check(err)
242+
}
243+
234244
if rpc.Call == "mkdirp" {
235-
err = os.MkdirAll(enforcePath(rpc.Args[0]), os.ModePerm)
236-
} else if rpc.Call == "mv" {
237-
err = os.Rename(enforcePath(rpc.Args[0]), enforcePath(rpc.Args[1]))
245+
err = os.MkdirAll(path0, os.ModePerm)
246+
} else if rpc.Call == "mv" && len(rpc.Args) == 2 {
247+
err = os.Rename(path0, path1)
238248
} else if rpc.Call == "rm" {
239-
err = os.RemoveAll(enforcePath(rpc.Args[0]))
249+
err = os.RemoveAll(path0)
250+
} else {
251+
err = errors.New("invalid rpc call")
240252
}
241253

242254
check(err)
243255
w.Write([]byte("ok"))
244256
}
245257

246-
func enforcePath(p string) string {
258+
func enforcePath(p string) (string, error) {
247259
joined := filepath.Join(rootPath, strings.TrimPrefix(p, *extraPath))
248260
fp, err := filepath.Abs(joined)
249-
sl, _ := filepath.EvalSymlinks(fp) // err skipped as it would error for unexistent files (RPC check). The actual behaviour is tested below
261+
sl, _ := filepath.EvalSymlinks(fp) // err skipped as it would error for inexistent files (RPC check). The actual behaviour is tested below
250262

251263
// panic if we had a error getting absolute path,
252264
// ... or if path doesnt contain the prefix path we expect,
253265
// ... or if we're skipping hidden folders, and one is requested,
254266
// ... or if we're skipping symlinks, path exists, and a symlink out of bound requested
255267
if err != nil || !strings.HasPrefix(fp, rootPath) || *skipHidden && strings.Contains(p, "/.") || !*symlinks && len(sl) > 0 && !strings.HasPrefix(sl, rootPath) {
256-
panic(errors.New("invalid path"))
268+
return "", errors.New("invalid path")
257269
}
258270

259-
return fp
271+
return fp, nil
260272
}
261273

262274
func main() {

test-fixture/b.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
B!!!
1+
B!!!
2+
test

ui/script.js

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,11 @@ function rpc (call, args, cb) {
153153
xhr.open('POST', location.origin + window.extraPath + '/rpc')
154154
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
155155
xhr.send(JSON.stringify({ call, args }))
156-
xhr.onload = cb
157-
xhr.onerror = () => flicker(sadBadge)
156+
xhr.onload = () => cb(false)
157+
xhr.onerror = () => {
158+
flicker(sadBadge)
159+
cb(true)
160+
}
158161
}
159162

160163
const mkdirCall = (path, cb) => rpc('mkdirp', [prependPath(path)], cb)
@@ -315,28 +318,36 @@ const textTypes = ['.txt', '.rtf', '.md', '.markdown', '.log', '.yaml', '.yml']
315318
const isTextFile = src => src && textTypes.find(type => src.toLocaleLowerCase().includes(type))
316319
let fileEdited
317320

318-
function saveText (quitting) {
321+
function saveText (cb) {
319322
const formData = new FormData()
320323
formData.append(fileEdited, editor.value)
321-
const path = encodeURIComponent(decodeURI(location.pathname) + fileEdited)
324+
const fname = fileEdited + ".swp"
325+
const path = encodeURIComponent(decodeURI(location.pathname) + fname)
322326
upload(0, formData, path, () => {
323327
toast.style.display = 'none'
324-
if (!quitting) return
325-
clearInterval(window.padTimer)
326-
window.onbeforeunload = null
327-
resetView()
328-
softPrev()
329-
refresh()
328+
cb()
330329
}, () => {
331330
toast.style.display = 'block'
332-
if (!quitting) return
333331
alert('cant save!\r\nleave window open to resume saving\r\nwhen connection back up')
334332
})
335333
}
336334

337335
function padOff () {
338336
if (!isEditorMode()) { return }
339-
saveText(true)
337+
const swapfile = fileEdited + ".swp"
338+
saveText(() => {
339+
mvCall(prependPath(swapfile), prependPath(fileEdited), err => {
340+
if (err) {
341+
alert('cant save!\r\nleave window open to resume saving\r\nwhen connection back up')
342+
return
343+
}
344+
clearInterval(window.padTimer)
345+
window.onbeforeunload = null
346+
resetView()
347+
softPrev()
348+
refresh()
349+
})
350+
})
340351
return true
341352
}
342353

0 commit comments

Comments
 (0)