package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "net/http" "net/http/httptest" "os" "path" "regexp" "strings" "time" "gogs.inhome.blapointe.com/local/args" "gogs.inhome.blapointe.com/local/gziphttp" "gogs.inhome.blapointe.com/local/notes-server/notes/md" ) 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, "ip", "ip to serve", "") 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, "log", "emit access logs", false) 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() accessLogging := fs.GetBool("log") if mdCss != "" { b, err := ioutil.ReadFile(mdCss) if err != nil { panic(err) } mdCss = fmt.Sprintf(` `, mdClass, mdClass, mdClass, mdClass, b, ) } ip := fs.Get("ip").GetString() p := strings.TrimPrefix(fs.Get("p").GetString(), ":") http.Handle("/", http.HandlerFunc(handler(userPass, https, ro, d, md, mdCss, mdClass, accessLogging))) log.Printf("Serving %s on HTTP port: %s\n", d, p) log.Fatal(http.ListenAndServe(ip+":"+p, nil)) } func handler(userPass string, https, ro bool, d string, md bool, mdCss, mdClass string, accessLogging bool) http.HandlerFunc { return withAccessLogging(accessLogging, httpsOnly(https, gzip(basicAuth(userPass, endpoints(ro, withDel(ro, withMD(d, md, mdCss, mdClass, fserve(d)))))))) } func withAccessLogging(enabled bool, h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.URL.Host == "" { r.URL.Host = r.Host } b, _ := json.Marshal(map[string]any{ "_ts": time.Now(), "client": map[string]any{ "ip": r.RemoteAddr, "forwarded-for": r.Header.Get("X-Forwarded-For"), "user-agent": r.UserAgent(), }, "request": map[string]any{ "method": r.Method, "url": r.URL.String(), "headers": r.Header, }, }) fmt.Printf("%s\n", b) h(w, r) } } 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 { gziphttp.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") } }