package main import ( "bytes" "flag" "fmt" "io" "mime" "os" "path" "path/filepath" "sort" "strconv" "strings" "time" "contnet.org/lib/cnm-go" "contnet.org/lib/cnm-go/cnmfmt" "contnet.org/lib/cnp-go" ) var ( flagSelect = flag.String("select", "byte,info,cnm", "comma-separated list of enabled selectors") flagListen = flag.String("listen", "localhost:25454", "host and/or port to listen on for CNP connections") flagDir = flag.String("dir", ".", "directory to serve files from") ) 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 byteSelect(w cnp.ResponseWriter, r *cnp.Request, f *os.File, stat os.FileInfo) { size := stat.Size() sel, selq := r.Select() rej := cnp.ErrorInvalid{Reason: "invalid byte selector"} ss := strings.Split(selq, "-") if len(ss) != 2 { panic(rej) } ua := uint64(0) var err error if ss[0] != "" { ua, err = strconv.ParseUint(ss[0], 10, 63) if err != nil { panic(rej) } f.Seek(int64(ua), os.SEEK_SET) } fr := io.Reader(f) if ss[1] != "" { var ub uint64 ub, err = strconv.ParseUint(ss[1], 10, 63) if err != nil { panic(rej) } if ub < ua { panic(rej) } ub++ fr = io.LimitReader(fr, int64(ub-ua)) if int64(ub) < size { size = int64(ub) } } size = size - int64(ua) if size < 0 { size = 0 } w.Response().SetSelect(sel, strconv.FormatInt(int64(ua), 10)+"-"+strconv.FormatInt(int64(ua)+size-1, 10)) w.Response().SetLength(size) if _, err := io.Copy(w, fr); err != nil { panic(err) } } func cnmSelect(w cnp.ResponseWriter, r *cnp.Request, f *os.File, stat os.FileInfo) { doc, err := cnm.ParseDocument(f) if err != nil { panic(err) } sel, selq := r.Select() w.Response().SetSelect(sel, selq) sdoc, err := doc.Select(selq) if err != nil { panic(cnp.ErrorInvalid{Reason: err.Error()}) } if sdoc == nil { return } if err = sdoc.Write(w); err != nil { panic(err) } } func infoSelect(w cnp.ResponseWriter, r *cnp.Request, f *os.File, stat os.FileInfo) { sel, selq := r.Select() if selq != "" { panic(cnp.ErrorInvalid{Reason: "invalid info selector"}) } resp := w.Response() resp.SetLength(stat.Size()) var buf bytes.Buffer resp.Write(&buf) resp.Header = cnp.NewHeader(cnp.IntentOK, nil) resp.SetLength(int64(buf.Len())) resp.SetSelect(sel, selq) if _, err := io.Copy(w, &buf); err != nil { panic(err) } } func noSelect(w cnp.ResponseWriter, r *cnp.Request, f *os.File, stat os.FileInfo) { w.Response().SetLength(stat.Size()) if _, err := io.Copy(w, f); err != nil { panic(err) } } func serveFile(w cnp.ResponseWriter, r *cnp.Request, f *os.File, stat os.FileInfo, typ string, enabled map[string]bool) { sel, _ := r.Select() if !enabled[sel] { noSelect(w, r, f, stat) return } switch sel { case "byte": byteSelect(w, r, f, stat) return case "info": infoSelect(w, r, f, stat) return case "cnm": if typ == "text/cnm" { cnmSelect(w, r, f, stat) return } } panic(cnp.ErrorNotSupported{Reason: "unsupported selector"}) } func server(addr, dir string, sel map[string]bool) { 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 } typ := guessType(f.Name()) w.Response().SetName(stat.Name()) w.Response().SetType(typ) serveFile(w, r, f, stat, typ, sel) }))) } func main() { flag.Parse() sel := map[string]bool{} if *flagSelect != "" { for _, s := range strings.Split(*flagSelect, ",") { switch s { case "byte", "info", "cnm": sel[s] = true default: panic("unknown selector: " + s) } } } server(*flagListen, *flagDir, sel) }