package main import ( "bytes" "errors" "fmt" "io" "io/ioutil" "gogs.inhome.blapointe.com/local/args" "gogs.inhome.blapointe.com/local/gziphttp" "gogs.inhome.blapointe.com/local/notes-server/notes/md" "gogs.inhome.blapointe.com/local/simpleserve/simpleserve" "log" "net/http" "net/http/httptest" "os" "path" "regexp" "strings" ) const ( ENDPOINT_UPLOAD = "__upload__" ENDPOINT_DELETE = "__delete__" ) var ( fs *args.ArgSet ) func main() { fs = args.NewArgSet() fs.Append(args.STRING, "p", "port to serve", "8100") fs.Append(args.STRING, "u", "user:pass for basic auth", "") fs.Append(args.BOOL, "md", "whether to render markdown as html", true) fs.Append(args.BOOL, "ro", "read only mode", false) fs.Append(args.BOOL, "https", "https only", false) fs.Append(args.STRING, "md-css", "css to load for md", "/dev/null") fs.Append(args.STRING, "md-class", "class to wrap md", "phb") fs.Append(args.STRING, "d", "static path to serve", "./public") if err := fs.Parse(); err != nil { panic(err) } d := fs.Get("d").GetString() userPass := fs.Get("u").GetString() md := fs.Get("md").GetBool() ro := fs.Get("ro").GetBool() https := fs.Get("https").GetBool() mdCss := fs.Get("md-css").GetString() mdClass := fs.Get("md-class").GetString() if mdCss != "" { b, err := ioutil.ReadFile(mdCss) if err != nil { panic(err) } mdCss = fmt.Sprintf(` `, mdClass, mdClass, mdClass, mdClass, b, ) } p := strings.TrimPrefix(fs.Get("p").GetString(), ":") http.Handle("/", http.HandlerFunc(handler(userPass, https, ro, d, md, mdCss, mdClass))) log.Printf("Serving %s on HTTP port: %s\n", d, p) log.Fatal(http.ListenAndServe(":"+p, nil)) } func handler(userPass string, https, ro bool, d string, md bool, mdCss, mdClass string) http.HandlerFunc { return httpsOnly(https, gzip(basicAuth(userPass, endpoints(ro, withDel(ro, withMD(d, md, mdCss, mdClass, fserve(d))))))) } func writeMeta(w http.ResponseWriter) { fmt.Fprintf(w, ` `) } func writeForm(w http.ResponseWriter) { fmt.Fprintf(w, `
`, ENDPOINT_UPLOAD) } func basicAuth(userPass string, foo http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if userPass != "" { u, p, ok := r.BasicAuth() if !ok || u+":"+p != userPass { w.Header().Set("WWW-Authenticate", "Basic") w.WriteHeader(401) return } } foo(w, r) } } func httpsOnly(https bool, foo http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if https && r.URL.Scheme != "https" { log.Printf("redirecting: %+v", r.URL) r.URL.Scheme = "https" http.Redirect(w, r, r.URL.String(), http.StatusSeeOther) return } foo(w, r) } } func gzip(foo http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if gziphttp.Can(r) { gz := gziphttp.New(w) defer gz.Close() w = gz } foo(w, r) } } func endpoints(ro bool, foo http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if isDir(r) { writeMeta(w) if !ro { writeForm(w) } } if !ro && isUploaded(r) { if err := upload(w, r); err != nil { fmt.Fprintln(w, err.Error()) } } else if !ro && isDeleted(r) { if err := del(w, r); err != nil { fmt.Fprintln(w, err.Error()) } } else { simpleserve.SetContentTypeIfMedia(w, r) foo(w, r) } } } func isUploaded(r *http.Request) bool { return path.Base(r.URL.Path) == ENDPOINT_UPLOAD } func isDeleted(r *http.Request) bool { return path.Base(r.URL.Path) == ENDPOINT_DELETE } func isDir(r *http.Request) bool { d := toRealPath(r.URL.Path) fi, err := os.Stat(d) if err != nil { return false } if !fi.IsDir() { return false } if _, err := os.Stat(path.Join(d, "index.html")); err == nil { return false } return true } func withMD(dir string, enabled bool, mdCss, mdClass string, foo http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { realpath := toRealPath(r.URL.Path) if enabled && !isDir(r) && path.Ext(realpath) == ".md" { b, err := ioutil.ReadFile(realpath) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } s, err := md.Gomarkdown(b, nil) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintln(w, mdCss) fmt.Fprintf(w, "
", mdClass) s = strings.ReplaceAll(s, "
") } else { foo(w, r) } } } func withDel(ro bool, foo http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !isDir(r) { foo(w, r) return } fmt.Fprintln(w, ``) w2 := httptest.NewRecorder() foo(w2, r) b := bytes.Split(w2.Body.Bytes(), []byte("\n")) buff := bytes.NewBuffer(nil) for i := range b { if !ro && bytes.Contains(b[i], []byte(" 0 { match = bytes.Split(match, []byte(`href="`))[1] match = match[:len(match)-1] b[i] = []byte(fmt.Sprintf(` %s`, match, ENDPOINT_DELETE, match, b[i])) } } buff.Write(b[i]) buff.Write([]byte("\n")) } io.Copy(w, buff) } } func fserve(d string) http.HandlerFunc { h := http.FileServer(http.Dir(d)) return h.ServeHTTP } func upload(w http.ResponseWriter, r *http.Request) error { r.ParseMultipartForm(100 << 20) file, handler, err := r.FormFile("file") if err != nil { return err } defer file.Close() p := toRealPath(path.Join(path.Dir(r.URL.Path), handler.Filename)) if fi, err := os.Stat(p); err == nil && !fi.IsDir() { return errors.New("already exists") } f, err := os.Create(p) if err != nil { return err } defer f.Close() if _, err := io.Copy(f, file); err != nil { return err } http.Redirect(w, r, path.Dir(r.URL.Path)+"/", http.StatusSeeOther) return nil } func del(w http.ResponseWriter, r *http.Request) error { p := toRealPath(path.Dir(r.URL.Path)) _, err := os.Stat(p) if err != nil { return err } err = os.RemoveAll(p) if err != nil { return err } http.Redirect(w, r, path.Dir(path.Dir(r.URL.Path))+"/", http.StatusSeeOther) return nil } func toRealPath(p string) string { d := path.Join(fs.Get("d").GetString()) return path.Join(d, p) } func setContentTypeIfMedia(w http.ResponseWriter, r *http.Request) { switch strings.ToLower(path.Ext(r.URL.Path)) { case ".mp4": w.Header().Set("Content-Type", "video/mp4") case ".mkv": w.Header().Set("Content-Type", "video/x-matroska") case ".mp3": w.Header().Set("Content-Type", "audio/mpeg3") case ".epub", ".mobi": w.Header().Set("Content-Disposition", "attachment") case ".jpg", ".jpeg": w.Header().Set("Content-Type", "image/jpeg") case ".gif": w.Header().Set("Content-Type", "image/gif") case ".png": w.Header().Set("Content-Type", "image/png") case ".ico": w.Header().Set("Content-Type", "image/x-icon") case ".svg": w.Header().Set("Content-Type", "image/svg+xml") case ".css": w.Header().Set("Content-Type", "text/css") case ".js": w.Header().Set("Content-Type", "text/javascript") case ".json": w.Header().Set("Content-Type", "application/json") case ".html", ".htm": w.Header().Set("Content-Type", "text/html") case ".pdf": w.Header().Set("Content-Type", "application/pdf") case ".webm": w.Header().Set("Content-Type", "video/webm") case ".weba": w.Header().Set("Content-Type", "audio/webm") case ".webp": w.Header().Set("Content-Type", "image/webp") case ".zip": w.Header().Set("Content-Type", "application/zip") case ".7z": w.Header().Set("Content-Type", "application/x-7z-compressed") case ".tar": w.Header().Set("Content-Type", "application/x-tar") } }