diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | cn-fileserver.service | 24 | ||||
-rw-r--r-- | fileserver.go | 231 | ||||
-rw-r--r-- | magic.go | 32 |
4 files changed, 288 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..226f507 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/cn-fileserver diff --git a/cn-fileserver.service b/cn-fileserver.service new file mode 100644 index 0000000..007585a --- /dev/null +++ b/cn-fileserver.service @@ -0,0 +1,24 @@ +[Unit] +Description=ContNet file server +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/cn-fileserver 0.0.0.0 /var/contnet/cnroot +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +InaccessibleDirectories=/home +ReadOnlyDirectories=/ +CapabilityBoundingSet= +LimitFSIZE=0 +DeviceAllow=/dev/null rw +MemoryDenyWriteExecute=yes +User=nobody +Group=nogroup +WorkingDirectory=/var/contnet/cnroot +Restart=always +RestartSec=10s + +[Install] +WantedBy=multi-user.target diff --git a/fileserver.go b/fileserver.go new file mode 100644 index 0000000..e1754a1 --- /dev/null +++ b/fileserver.go @@ -0,0 +1,231 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "mime" + "os" + "path" + "path/filepath" + "sort" + "strings" + "time" + + "contnet.org/lib/cnm-go" + "contnet.org/lib/cnm-go/cnmfmt" + "contnet.org/lib/cnp-go" +) + +func init() { + mime.AddExtensionType(".cnm", "text/cnm") +} + +func listdir(w cnp.ResponseWriter, r *cnp.Request, root string, f *os.File, stat os.FileInfo) { + if p := r.Path(); !strings.HasSuffix(p, "/") { + w.Response().SetIntent(cnp.IntentRedirect) + w.Response().SetLocation("", p+"/") + return + } + + files, err := f.Readdir(-1) + if err != nil { + panic(cnp.ErrorServerError{Reason: err.Error()}) + } + + sort.Slice(files, func(i, j int) bool { + d1, d2 := files[i].IsDir(), files[j].IsDir() + if !d1 && d2 { + return false + } + if d1 && !d2 { + return true + } + return files[i].Name() < files[j].Name() + }) + + relpath, err := filepath.Rel(root, f.Name()) + if err != nil { + panic(cnp.ErrorServerError{Reason: err.Error()}) + } + dirpath := filepath.ToSlash(relpath) + site := cnm.Site{Path: dirpath} + dirname := path.Clean("/" + dirpath) + if !strings.HasSuffix(dirname, "/") { + dirname += "/" + } + + var par []cnmfmt.Text + var links []cnm.Link + + if dirname != "/" { + links = []cnm.Link{cnm.Link{ + URL: "../", + Name: "..", + Description: "Go to the parent directory", + }} + par = append(par, cnmfmt.Text{Spans: []cnmfmt.Span{cnmfmt.Span{ + //Format: cnmfmt.Format{Link: path.Dir(dirname)}, + Format: cnmfmt.Format{Link: "../"}, + Text: "..", + }}}) + } + + for _, file := range files { + name := file.Name() + if name[0] == '.' { + continue + } + if file.IsDir() { + name += "/" + } + //site.Children = append(site.Children, cnm.Site{Path: name, Name: name}) + par = append(par, cnmfmt.Text{Spans: []cnmfmt.Span{cnmfmt.Span{ + Format: cnmfmt.Format{Link: dirname + name}, + Text: name, + }}}) + } + + content := cnm.NewContentBlock("content") + content.AppendChild(cnmfmt.NewTextFmtBlock(par)) + + if site.Path != "." { + site = cnm.Site{Children: []cnm.Site{site}} + } + + doc := &cnm.Document{ + Title: dirname, + Content: content, + Site: site, + Links: links, + } + + var buf bytes.Buffer + if err = doc.Write(&buf); err != nil { + panic(cnp.ErrorServerError{Reason: err.Error()}) + } + + w.Response().SetType("text/cnm") + w.Response().SetLength(int64(buf.Len())) + + if _, err = io.Copy(w, &buf); err != nil { + panic(cnp.ErrorServerError{Reason: err.Error()}) + } +} + +func guessType(fname string) string { + typ := mime.TypeByExtension(path.Ext(fname)) + if typ == "" { + typ, _ = GetMimeType(fname) + } + t, _, _ := mime.ParseMediaType(typ) + return t +} + +func open(dir, fpath string) (*os.File, error) { + if dir == "" { + dir = "." + } + if filepath.Separator != '/' && strings.ContainsRune(fpath, filepath.Separator) { + // prevent exiting root dir on e.g. Windows + // TODO: maybe filepath.Clean instead? + return nil, cnp.ErrorRejected{Reason: "invalid filepath"} + } + return os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+fpath)))) +} + +func getfile(dir, pth string, strict bool) (*os.File, os.FileInfo) { + var f *os.File + var err error + fnames := []string{pth} + if !strings.HasSuffix(pth, ".cnm") { + fnames = []string{pth, pth + ".cnm"} + } + var firstErr error + for _, fname := range fnames { + f, err = open(dir, fname) + if firstErr == nil { + firstErr = err + } + if os.IsNotExist(err) { + //panic(cnp.ErrorNotFound{Reason: err.Error()}) + continue + } else if _, ok := err.(cnp.Error); ok { + panic(err) + } else if err != nil { + panic(cnp.ErrorServerError{Reason: err.Error()}) + } + break + } + var stat os.FileInfo + if err != nil { + if strict { + return nil, stat + } + panic(cnp.ErrorNotFound{Reason: firstErr.Error()}) + } + stat, err = f.Stat() + if err != nil { + panic(cnp.ErrorServerError{Reason: err.Error()}) + } + if stat.IsDir() { + if !strings.HasSuffix(pth, "/") { + return nil, stat + } + f2, st2 := getfile(dir, pth+"index.cnm", true) + if f2 != nil { + return f2, st2 + } + } + return f, stat +} + +func server(addr, dir string) { + fmt.Printf("listening on %q, serving dir %q\n", addr, dir) + panic(cnp.ListenAndServe(addr, cnp.HandlerFunc(func(w cnp.ResponseWriter, r *cnp.Request) { + f, stat := getfile(dir, r.Path(), false) + defer f.Close() + + mtime := stat.ModTime().Truncate(time.Second) + + w.Response().SetModified(mtime) + w.Response().SetTime(time.Now()) + if stat.IsDir() { + listdir(w, r, dir, f, stat) + return + } + + if ifmod := r.IfModified(); !ifmod.IsZero() && (ifmod.Equal(mtime) || ifmod.After(mtime)) { + w.Response().SetIntent(cnp.IntentNotModified) + if err := w.WriteHeader(); err != nil { + panic(err) + } + return + } + + w.Response().SetLength(stat.Size()) + w.Response().SetName(stat.Name()) + w.Response().SetType(guessType(f.Name())) + + if _, err := io.Copy(w, f); err != nil { + panic(err) + } + }))) +} + +func main() { + if len(os.Args) > 3 { + prog := path.Base(os.Args[0]) + fmt.Fprintf(os.Stderr, "%s: usage: %s [HOST[:PORT]] [DIR]", prog, prog) + os.Exit(2) + } + addr := "localhost" + dir := "." + if len(os.Args) >= 2 { + addr = os.Args[1] + } + if len(os.Args) >= 3 { + dir = os.Args[2] + } + server(addr, dir) +} diff --git a/magic.go b/magic.go new file mode 100644 index 0000000..a6fb48d --- /dev/null +++ b/magic.go @@ -0,0 +1,32 @@ +package main + +// #cgo LDFLAGS: -lmagic +// #include <stdlib.h> +// #include <magic.h> +import "C" +import "errors" +import "unsafe" + +var magic C.magic_t + +func init() { + magic = C.magic_open(C.MAGIC_MIME_TYPE | C.MAGIC_SYMLINK | C.MAGIC_ERROR) + if magic == nil { + panic("unable to initialize libmagic") + } + if C.magic_load(magic, nil) != 0 { + C.magic_close(magic) + panic("unable to load libmagic database: " + C.GoString(C.magic_error(magic))) + } +} + +// GetMimeType uses libmagic to find the mime type of a file by filename. +func GetMimeType(fname string) (string, error) { + cfname := C.CString(fname) + defer C.free(unsafe.Pointer(cfname)) + mime := C.magic_file(magic, cfname) + if mime == nil { + return "", errors.New(C.GoString(C.magic_error(magic))) + } + return C.GoString(mime), nil +} |