Skip to content

Commit

Permalink
Merge pull request #26 from pldubouilh/prefixed-path
Browse files Browse the repository at this point in the history
prefixed ui + symlink handling
  • Loading branch information
pldubouilh committed Jul 27, 2019
2 parents 881d583 + 49d0f3f commit cb6f093
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 98 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -34,3 +34,5 @@ test-fixture/*/*
!test-fixture/hols/glasgow.jpg
!test-fixture/hols/landscape-540116_1920.jpg
!test-fixture/hols/scotland-1761292_1920.jpg
!test-fixture/fancy-path
!test-fixture/fancy-path/*
13 changes: 11 additions & 2 deletions Makefile
Expand Up @@ -9,15 +9,24 @@ run:
make build
./gossa test-fixture

run-extra:
make build
./gossa -prefix="/fancy-path/" -symlinks=true test-fixture

ci:
-@cd test-fixture && ln -s ../docker .
timeout 10 make run &
cp src/gossa_test.go . && sleep 5 && go test
sleep 11 && timeout 10 make run-extra &
cp src/gossa_test.go . && go test
rm gossa_test.go

watch:
ls src/* gossa-ui/* | entr -rc make run

ci-watch:
watch-extra:
ls src/* gossa-ui/* | entr -rc make run-extra

watch-ci:
ls src/* gossa-ui/* | entr -rc make ci

build-all:
Expand Down
2 changes: 1 addition & 1 deletion gossa-ui
Submodule gossa-ui updated 2 files
+43 −54 script.js
+4 −1 ui.tmpl
4 changes: 3 additions & 1 deletion readme.md
Expand Up @@ -8,7 +8,7 @@ gossa
[![docker pulls](https://img.shields.io/docker/pulls/pldubouilh/gossa.svg?logo=docker)](https://hub.docker.com/r/pldubouilh/gossa)
[![github downloads](https://img.shields.io/github/downloads/pldubouilh/gossa/total.svg?logo=github)](https://github.com/pldubouilh/gossa/releases)

a fast and simple webserver for your files, that's dependency-free and with only 210 lines of code, easy to review.
a fast and simple webserver for your files, that's dependency-free and with under 200 lines of code, easy to review.

a [simple UI](https://github.com/pldubouilh/gossa-ui) comes as default, featuring :

Expand All @@ -26,6 +26,8 @@ built blobs are available on the [release page](https://github.com/pldubouilh/go

### usage
```sh
% ./gossa --help

% ./gossa -h 192.168.100.33 ~/storage
```

Expand Down
126 changes: 57 additions & 69 deletions src/gossa.go
Expand Up @@ -20,6 +20,8 @@ import (

var host = flag.String("h", "127.0.0.1", "host to listen to")
var port = flag.String("p", "8001", "port to listen to")
var extraPath = flag.String("prefix", "/", "url prefix at which gossa can be reached, e.g. /gossa/ (slashes of importance)")
var symlinks = flag.Bool("symlinks", false, "follow symlinks \033[4mWARNING\033[0m: symlinks will by nature allow to escape the defined path (default: false)")
var verb = flag.Bool("verb", true, "verbosity")
var skipHidden = flag.Bool("k", true, "skip hidden files")
var initPath = "."
Expand All @@ -36,6 +38,7 @@ type rowTemplate struct {

type pageTemplate struct {
Title template.HTML
ExtraPath template.HTML
RowsFiles []rowTemplate
RowsFolders []rowTemplate
}
Expand All @@ -51,135 +54,122 @@ func check(e error) {
}
}

func logVerb(s ...interface{}) {
if *verb {
func exitPath(w http.ResponseWriter, s ...interface{}) {
if r := recover(); r != nil {
log.Println("error", s, r)
w.Write([]byte("error"))
} else if *verb {
log.Println(s...)
}
}

func sizeToString(bytes int64) string {
units := [9]string{"B", "k", "M", "G", "T", "P", "E", "Z", "Y"}
func humanize(bytes int64) string {
b := float64(bytes)
u := 0
for {
if b < 1024 {
return strconv.FormatFloat(b, 'f', 1, 64) + units[u]
return strconv.FormatFloat(b, 'f', 1, 64) + [9]string{"B", "k", "M", "G", "T", "P", "E", "Z", "Y"}[u]
}
b = b / 1024
u++
}
}

func replyList(w http.ResponseWriter, path string) {
func replyList(w http.ResponseWriter, fullPath string, path string) {
_files, err := ioutil.ReadDir(fullPath)
check(err)

if !strings.HasSuffix(path, "/") {
path += "/"
}

_files, err := ioutil.ReadDir(initPath + path)
check(err)

title := "/" + strings.TrimPrefix(path, *extraPath)
p := pageTemplate{}
if path != "/" {
if path != *extraPath {
p.RowsFolders = append(p.RowsFolders, rowTemplate{"../", "../", "", "folder"})
}
p.ExtraPath = template.HTML(html.EscapeString(*extraPath))
p.Title = template.HTML(html.EscapeString(title))

for _, el := range _files {
name := el.Name()
href := url.PathEscape(name)
if *skipHidden && strings.HasPrefix(name, ".") {
if *skipHidden && strings.HasPrefix(el.Name(), ".") {
continue
}
el, _ = os.Stat(fullPath + "/" + el.Name())
href := url.PathEscape(el.Name())
if el.IsDir() && strings.HasPrefix(href, "/") {
href = strings.Replace(href, "/", "", 1)
}
if el.IsDir() {
p.RowsFolders = append(p.RowsFolders, rowTemplate{name + "/", template.HTML(href), "", "folder"})
p.RowsFolders = append(p.RowsFolders, rowTemplate{el.Name() + "/", template.HTML(href), "", "folder"})
} else {
sl := strings.Split(name, ".")
sl := strings.Split(el.Name(), ".")
ext := strings.ToLower(sl[len(sl)-1])
p.RowsFiles = append(p.RowsFiles, rowTemplate{name, template.HTML(href), sizeToString(el.Size()), ext})
p.RowsFiles = append(p.RowsFiles, rowTemplate{el.Name(), template.HTML(href), humanize(el.Size()), ext})
}
}

p.Title = template.HTML(html.EscapeString(path))
page.Execute(w, p)
}

func doContent(w http.ResponseWriter, r *http.Request) {
path := html.UnescapeString(r.URL.Path)
fullPath, errPath := checkPath(path)
stat, errStat := os.Stat(fullPath)

if errStat != nil || errPath != nil {
logVerb("Error", errStat, errPath)
w.Write([]byte("error"))
if !strings.HasPrefix(r.URL.Path, *extraPath) {
http.Redirect(w, r, *extraPath, 302)
return
}

path := html.UnescapeString(r.URL.Path)
defer exitPath(w, "get content", path)
fullPath := checkPath(path)
stat, errStat := os.Stat(fullPath)
check(errStat)

if stat.IsDir() {
logVerb("Get list", fullPath)
replyList(w, path)
replyList(w, fullPath, path)
} else {
logVerb("Get file", fullPath)
fs.ServeHTTP(w, r)
}
}

func upload(w http.ResponseWriter, r *http.Request) {
unescaped, _ := url.PathUnescape(r.Header.Get("gossa-path"))
fullPath, err := checkPath(unescaped)

logVerb("Up", err, fullPath)
if err != nil {
w.Write([]byte("error"))
return
}

path, _ := url.PathUnescape(r.Header.Get("gossa-path"))
defer exitPath(w, "upload", path)
reader, _ := r.MultipartReader()
part, _ := reader.NextPart()
dst, _ := os.Create(fullPath)
dst, _ := os.Create(checkPath(path))
io.Copy(dst, part)
logVerb("Done upping", fullPath)
w.Write([]byte("ok"))
}

func rpc(w http.ResponseWriter, r *http.Request) {
var err error
var rpc rpcCall
bodyBytes, _ := ioutil.ReadAll(r.Body)
bodyString := string(bodyBytes)
var payload rpcCall
json.Unmarshal([]byte(bodyString), &payload)

for i := range payload.Args {
payload.Args[i], err = checkPath(payload.Args[i])
if err != nil {
logVerb("Cant read path", err, payload)
w.Write([]byte("error"))
return
}
}

if payload.Call == "mkdirp" {
err = os.MkdirAll(payload.Args[0], os.ModePerm)
} else if payload.Call == "mv" {
err = os.Rename(payload.Args[0], payload.Args[1])
} else if payload.Call == "rm" {
err = os.RemoveAll(payload.Args[0])
json.Unmarshal(bodyBytes, &rpc)
defer exitPath(w, "rpc", rpc)

if rpc.Call == "mkdirp" {
err = os.MkdirAll(checkPath(rpc.Args[0]), os.ModePerm)
} else if rpc.Call == "mv" {
err = os.Rename(checkPath(rpc.Args[0]), checkPath(rpc.Args[1]))
} else if rpc.Call == "rm" {
err = os.RemoveAll(checkPath(rpc.Args[0]))
}

logVerb("RPC", err, payload)
check(err)
w.Write([]byte("ok"))
}

func checkPath(p string) (string, error) {
p = filepath.Join(initPath, p)
func checkPath(p string) string {
p = filepath.Join(initPath, strings.TrimPrefix(p, *extraPath))
fp, err := filepath.Abs(p)
sl, _ := filepath.EvalSymlinks(fp)

if err != nil || !strings.HasPrefix(fp, initPath) {
return "", errors.New("error")
if err != nil || !strings.HasPrefix(fp, initPath) || len(sl) > 0 && !*symlinks && !strings.HasPrefix(sl, initPath) {
panic(errors.New("invalid path"))
}

return fp, nil
return fp
}

func main() {
Expand All @@ -194,14 +184,12 @@ func main() {

hostString := *host + ":" + *port
fmt.Println("Gossa startig on directory " + initPath)
fmt.Println("Listening on http://" + hostString)

root := http.Dir(initPath)
fs = http.StripPrefix("/", http.FileServer(root))
fmt.Println("Listening on http://" + hostString + *extraPath)

http.HandleFunc("/rpc", rpc)
http.HandleFunc("/post", upload)
http.HandleFunc(*extraPath+"rpc", rpc)
http.HandleFunc(*extraPath+"post", upload)
http.HandleFunc("/", doContent)
fs = http.StripPrefix(*extraPath, http.FileServer(http.Dir(initPath)))
err = http.ListenAndServe(hostString, nil)
check(err)
}

0 comments on commit cb6f093

Please sign in to comment.