package main import ( "bytes" "crypto/tls" "encoding/json" "flag" "fmt" "html/template" "io" "log" "mime" "net" "net/http" "os" "path" "sort" "strings" "time" "github.com/russross/blackfriday/v2" ) var ( listen = flag.String("l", ":8080", "set http listener to [ip]:port") root = flag.String("root", "root", "werc webroot") indexFiles = []string{"index", "README"} ) type WercConfig struct { MasterSite string Title string Subtitle string Lang string } type MenuEntry struct { Name string Path string This bool Sub []*MenuEntry } type MenuEntries []*MenuEntry func (me MenuEntries) Len() int { return len(me) } func (me MenuEntries) Swap(i, j int) { me[i], me[j] = me[j], me[i] } func (me MenuEntries) Less(i, j int) bool { return me[i].Name < me[j].Name } type WercPage struct { Title string // title Menu MenuEntries // menu entries Content template.HTML // inner page content Config WercConfig // site-specific config } type Werc struct { root string conf WercConfig tmpl *template.Template fs *FS } func New(root string) *Werc { w := new(Werc) w.root = root w.tmpl = template.New("root") var err error w.fs, err = NewFS(root) if err != nil { log.Printf("can't open root: %v", err) return nil } b, err := readfile(w.fs, "etc/config.json") if err != nil { log.Printf("error loading config.json: %v", err) return nil } err = json.Unmarshal(b, &w.conf) if err != nil { log.Printf("%s: %s", root+"/config.json", err) return nil } // load templates tmpls := []string{"base", "directory", "footer", "menu", "text", "topbar"} for _, tn := range tmpls { b, err := readfile(w.fs, fmt.Sprintf("lib/%s.html", tn)) if err != nil { panic(err) } template.Must(w.tmpl.New(tn + ".html").Parse(string(b))) } return w } func cleanname(s string) string { if s == "_werc" { return "" } if strings.HasPrefix(s, "index") { return "" } switch s { case "sitemap.txt", "sitemap.gz": return "" } for _, suf := range []string{".md", ".txt", ".html"} { if strings.HasSuffix(s, suf) { return strings.TrimSuffix(s, suf) } } return s } func ptitle(s string) string { s = strings.TrimSuffix(s, "/") if idx := strings.LastIndex(s, "index"); idx != -1 { s = s[:idx-1] } _, file := path.Split(s) for _, suf := range []string{".md", ".txt", ".html"} { if strings.HasSuffix(file, suf) { return strings.TrimSuffix(file, suf) } } return file } func (werc *Werc) genmenu(site, dir string) MenuEntries { var dirs []string var root MenuEntries base := "sites/" + site spl := strings.Split(strings.TrimPrefix(path.Clean(dir), "/"), "/") _, current := path.Split(dir) if current != "" { spl = spl[:len(spl)-1] } //log.Printf("base %s path %s spl %+v", base, dir, spl) dirs = append(dirs, "/") for i := range spl { path := "/" + path.Join(spl[:i+1]...) dirs = append(dirs, path) } //log.Printf("dirs %v", dirs) var last MenuEntries for i := range dirs { var sub MenuEntries b := path.Join(base, dirs[i]) fi, _ := readdir(werc.fs, b) for _, f := range fi { newname, ok := okmenu(b, f) if !ok { continue } me := &MenuEntry{Name: newname, Path: path.Join(dirs[i], newname)} if f.Mode().IsDir() { me.Path = me.Path + "/" me.Name = me.Name + "/" } // if browing a file, mark it as current if me.Name == current { me.This = true } //log.Printf("me %+v", me) sub = append(sub, me) } if sub != nil { sort.Sort(sub) } if dirs[i] == "/" { root = sub last = root } else { // mark directories as currently being browsed for l, v := range last { _, file := path.Split(dirs[i]) if v.Name == file+"/" { last[l].This = true last[l].Sub = sub last = sub } } } } sort.Sort(root) return root } func (werc *Werc) WercCommon(w http.ResponseWriter, r *http.Request, site string, page *WercPage) { path := r.URL.Path page.Menu = werc.genmenu(site, path) conf := "sites/" + site + "/_werc/config.json" b, err := readfile(werc.fs, conf) if err != nil { log.Printf("%s: %s", conf, err) } else { err = json.Unmarshal(b, &page.Config) if err != nil { log.Printf("%s: %s", conf, err) } } //log.Printf("root %+v", page.Menu) if err := werc.tmpl.ExecuteTemplate(w, "base.html", page); err != nil { log.Printf("%s: %s", r.URL, err) } } // returns true if a path name is ok to show in the navigation func okmenu(base string, fi os.FileInfo) (string, bool) { if strings.HasPrefix(fi.Name(), ".") { return "", false } if fi.Name() == "_werc" { return "", false } for _, index := range indexFiles { if strings.HasPrefix(fi.Name(), index+".") { return "", false } } if strings.Contains(fi.Name(), "sitemap.") { return "", false } if fi.Mode().IsDir() { return fi.Name(), true } for _, suf := range []string{".md", ".txt", ".html"} { if strings.HasSuffix(fi.Name(), suf) { return strings.TrimSuffix(fi.Name(), suf), true } } return "", false } func (werc *Werc) WercDir(w http.ResponseWriter, r *http.Request, site, dir string) { type DirEntry struct { Name string Fi os.FileInfo } type DirData struct { Title string Entries []DirEntry } var data DirData data.Title = r.URL.Path buf := new(bytes.Buffer) fi, err := readdir(werc.fs, dir) if err != nil { http.Error(w, fmt.Sprintf("%s", err), 500) return } for _, f := range fi { if name := cleanname(f.Name()); name != "" { e := DirEntry{Name: name, Fi: f} data.Entries = append(data.Entries, e) } } werc.tmpl.ExecuteTemplate(buf, "directory.html", data) werc.WercCommon(w, r, site, &WercPage{Title: ptitle(dir), Content: template.HTML(buf.String())}) } func (werc *Werc) WercMd(w http.ResponseWriter, r *http.Request, site, path string) { b, err := readfile(werc.fs, path) if err != nil { http.Error(w, fmt.Sprintf("%s", err), 404) return } md := blackfriday.Run(b) werc.WercCommon(w, r, site, &WercPage{Title: ptitle(path), Content: template.HTML(string(md))}) } func (werc *Werc) WercHTML(w http.ResponseWriter, r *http.Request, site, path string) { b, err := readfile(werc.fs, path) if err != nil { http.Error(w, fmt.Sprintf("%s", err), 404) return } werc.WercCommon(w, r, site, &WercPage{Title: ptitle(path), Content: template.HTML(string(b))}) } func (werc *Werc) WercTXT(w http.ResponseWriter, r *http.Request, site, path string) { b, err := readfile(werc.fs, path) if err != nil { http.Error(w, fmt.Sprintf("%s", err), 404) return } buf := new(bytes.Buffer) werc.tmpl.ExecuteTemplate(buf, "text.html", string(b)) werc.WercCommon(w, r, site, &WercPage{Title: ptitle(path), Content: template.HTML(buf.String())}) } func (werc *Werc) Pub(w http.ResponseWriter, r *http.Request, route string) { strings.TrimPrefix(route, "/") b, err := readfile(werc.fs, route) if err != nil { log.Printf("Pub: %v", err) http.Error(w, err.Error(), 404) return } buf := bytes.NewReader(b) http.ServeContent(w, r, path.Base(route), time.Now(), buf) log.Printf("pub sent %d bytes", len(b)) } func (werc *Werc) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Printf("%s", r.URL) site, _, _ := net.SplitHostPort(r.Host) if site == "" { site = werc.conf.MasterSite } route := r.URL.Path // try pub first if strings.HasPrefix(route, "/pub") { werc.Pub(w, r, route) return } again: base := "sites/" + site if strings.HasSuffix(route, "/index") { http.Redirect(w, r, strings.TrimSuffix(route, "/index"), http.StatusMovedPermanently) return } if !strings.HasSuffix(route, "/") { f, err := werc.fs.Open(base + route) if err == nil { defer f.Close() fi, err := f.Stat() if err != nil && fi.IsDir() { http.Redirect(w, r, route+"/", http.StatusMovedPermanently) return } } } // various format handling sufferring := map[string]func(w http.ResponseWriter, r *http.Request, site, path string){ "md": werc.WercMd, "html": werc.WercHTML, "txt": werc.WercTXT, } log.Printf("path %v", base) for suf, handler := range sufferring { var tryfiles []string if strings.HasSuffix(route, "/") { for _, index := range indexFiles { tryfiles = append(tryfiles, path.Join(base, route, index+"."+suf)) } } else { tryfiles = append(tryfiles, path.Join(base, route+"."+suf)) } for _, f := range tryfiles { fh, err := werc.fs.Open(f) if err != nil { continue } defer fh.Close() log.Printf("%s %s", suf, f) handler(w, r, site, f) return } } if f, err := werc.fs.Open(base + route); err == nil { defer f.Close() st, _ := f.Stat() if st.Mode().IsDir() { // directory handling log.Printf("d %s", base+route) werc.WercDir(w, r, site, base+route) return } // plain file handling log.Printf("f %s", base+route) // ripped from http.serveContent ctype := mime.TypeByExtension(path.Ext(route)) if ctype == "" { // read a chunk to decide between utf-8 text and binary var buf [512]byte n, _ := io.ReadFull(f, buf[:]) ctype = http.DetectContentType(buf[:n]) _, err := f.Seek(0, os.SEEK_SET) // rewind to output whole file if err != nil { http.Error(w, "seeker can't seek", http.StatusInternalServerError) return } } w.Header().Set("Content-Type", ctype) io.Copy(w, f) return } if site != werc.conf.MasterSite { site = werc.conf.MasterSite goto again } log.Printf("404 %s", route) http.NotFound(w, r) } func main() { flag.Parse() w := New(*root) if w == nil { log.Fatal("can't create root") } mux := http.NewServeMux() mux.Handle("/", w) var listener net.Listener var tlsconf *tls.Config var err error listener, err = net.Listen("tcp", *listen) if err != nil { log.Fatal(err) } s := &http.Server{ Addr: *listen, Handler: mux, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, TLSConfig: tlsconf, } fmt.Println("Listening on", *listen) log.Fatal(s.Serve(listener)) }