package web import ( "context" _ "embed" "errors" "fmt" "github.com/microcosm-cc/bluemonday" "golang.org/x/sys/unix" "html" "html/template" "mime" "net/http" "net/mail" "net/url" "os" "os/signal" "path/filepath" "strings" "git.sophuwu.com/mailboxxer/db" ) //go:embed templates/index.html var htmlTemplate string //go:embed templates/send.html var sendTemplate string //go:embed templates/style.css var cssText string //go:embed templates/script.js var jsText string var t *template.Template const DefaultAddr = "127.0.1.69:3141" var sigchan = make(chan os.Signal) func stopServer() { sigchan <- unix.SIGTERM } func load() { fmt.Printf("starting %s...\n", filepath.Base(os.Args[0])) fmt.Printf("\tPID: %d\n", os.Getpid()) ld := func() { // systemd reload signal } ld() ch := make(chan os.Signal) signal.Notify(ch, unix.SIGHUP) for { <-ch ld() } } func ServeHttp(addr string) { // go load() t = template.Must(template.New("index").Parse(htmlTemplate)) signal.Notify(sigchan, unix.SIGTERM, unix.SIGINT, unix.SIGQUIT, unix.SIGKILL, unix.SIGSTOP, unix.SIGABRT) server := http.Server{ Addr: addr, Handler: HttpHandle{http.FileServer(http.Dir(db.SAVEPATH))}, } go func() { fmt.Printf("starting http server on: %s\n", addr) err := server.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { fmt.Fprintln(os.Stderr, "error: server:", err) stopServer() } fmt.Println("stopped") }() sig := <-sigchan fmt.Printf("\rgot %s\nstopping server...\n", sig.String()) _ = server.Shutdown(context.Background()) } func E(s ...string) []any { a := make([]any, len(s)) for i, v := range s { a[i] = html.EscapeString(v) } return a } type HtmlEM struct { Id string Date string Subject string ToName string ToAddr string FromName string FromAddr string } func ParseInt(s string) int { n := 0 for _, r := range s { if r < '0' || r > '9' { break } n = n*10 + int(r-'0') } return n } func TempErr(w http.ResponseWriter, code int) { msg := "An error occurred" switch code { case 404: msg = "Not found" case 500: msg = "Internal server error" } var dat = map[string]any{ "Error": msg, "ErrorCode": code, } t.Execute(w, dat) } type HttpHandle struct { fs http.Handler } func (hh HttpHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) { // w.Header().Add("Content-Security-Policy", `default-src 'none'; style-src 'self'; img-src https: data: cid:; font-src 'self';`) // w.Header().Add("X-Content-Type-Options", "nosniff") // w.Header().Add("X-Frame-Options", "DENY") w.Header().Add("Referrer-Policy", "no-referrer") // w.Header().Add("Cross-Origin-Opener-Policy", "same-origin") // w.Header().Add("Cross-Origin-Embedder-Policy", "same-origin") // w.Header().Add("Cross-Origin-Resource-Policy", "same-origin") if r.URL.Path == "/style.css" { w.Header().Set("Content-Type", "text/css; charset=utf-8") fmt.Fprint(w, cssText) return } if r.URL.Path == "/script.js" { w.Header().Set("Content-Type", "application/javascript; charset=utf-8") fmt.Fprint(w, jsText) return } if r.URL.Path == "/send.html" { w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, sendTemplate) return } h := NewHandle(w, r) r.ParseForm() if r.URL.Path == "/" || r.URL.Path == "/api" { h.SearchHandler() return } if r.URL.Path == "/open" { w.Header().Set("Content-Type", "text/html; charset=utf-8") id := r.Form.Get("id") if len(id) != 40 { TempErr(w, 404) return } b, err := os.ReadFile(filepath.Join(db.SAVEPATH, id, "body.html")) var html string if err != nil { b, err = os.ReadFile(filepath.Join(db.SAVEPATH, id, "body.txt")) if err != nil { TempErr(w, 404) return } html = fmt.Sprintf(`
%s
`, string(b)) } else { html = string(b) p := bluemonday.UGCPolicy() p.AllowDataURIImages() p.AllowIFrames() p.RequireSandboxOnIFrame() p.AllowStyling() p.AllowImages() p.AllowComments() p.AllowRelativeURLs(true) p.AllowURLSchemes("http", "https", "cid", "data") p.AllowStandardAttributes() p.AllowAttrs("style", "class").Globally() p.AllowAttrs("width", "height", "src", "rel").OnElements("img", "iframe", "video", "audio") p.AllowAttrs("href", "target").OnElements("a") p.AllowTables() p.AllowLists() p.AllowElements("table", "thead", "tbody", "tfoot", "th", "tr", "td", "ul", "ol", "li", "dl", "dt", "dd", "style", "a", "img", "iframe", "video", "audio") // p.AllowAttrs("type").OnElements("style") p.AllowElements("div", "section", "span", "p", "br", "hr", "b", "i", "u", "strong", "em", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "code", "blockquote") // p.SkipElementsContent("script") p.AllowRelativeURLs(true) p.RewriteSrc(func(src *url.URL) { if _, err = os.Stat(filepath.Join(db.SAVEPATH, id, src.Path)); err == nil && strings.HasPrefix(mime.TypeByExtension(filepath.Ext(src.Path)), "image") { src.Path = filepath.Join("/" + id + "/" + src.Path) } }) html = p.Sanitize(html) if !strings.Contains(html, "") && !strings.Contains(html, "") { html = fmt.Sprintf(`%s`, html) } } html = strings.Replace(html, ``, ``, 1) fmt.Fprint(w, html) return } hh.fs.ServeHTTP(w, r) } type Handle struct { qu *db.Query w http.ResponseWriter r *http.Request } func NewHandle(w http.ResponseWriter, r *http.Request) *Handle { qu, errr := db.NewQuery(30) if errr != nil { fmt.Fprintln(os.Stderr, "Error creating query:", errr) db.Close() os.Exit(1) } return &Handle{ qu: qu, w: w, r: r, } } func Http() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // if filepath.Base(r.URL.Path) == "html.html" && len(strings.Split(r.URL.Path[1:], "/")) == 2 { // id := filepath.Base(filepath.Dir(r.URL.Path)) // s, _ := os.ReadFile(filepath.Join(db.SAVEPATH, id, "header.txt")) // b, _ := os.ReadFile(filepath.Join(db.SAVEPATH, id, "body.txt")) // fmt.Fprintf(w, `%s // //
%s

%s
// // `, id, string(s), string(b), "/"+id) // return // } if func(w http.ResponseWriter, r *http.Request) bool { var file string var id string l := strings.SplitN(r.URL.Path[1:], "/", 2) if len(l) == 0 { return false } if len(l) >= 1 { id = l[0] } if len(l) >= 2 { file = l[1] } if file != "" && filepath.Ext(file) != ".txt" { return false } de, err := os.ReadDir(filepath.Join(db.SAVEPATH, id)) if err != nil { return false } darkMode := r.URL.Query().Get("dark") if darkMode == "" || darkMode == "0" || darkMode == "false" { darkMode = "0" } else { darkMode = "1" } dat := map[string]any{ "DarkMode": darkMode, "Style": `style="overflow: auto;"`, } var ss string var f os.DirEntry if file != "" { var b []byte for _, f = range de { if f.Name() == file && f.Type().IsRegular() { b, err = os.ReadFile(filepath.Join(db.SAVEPATH, id, file)) break } } if err != nil || len(b) == 0 { return false } ss = string(b) if filepath.Ext(f.Name()) == ".txt" { dat["Text"] = ss } else { return false } } else { for _, f = range de { ss += fmt.Sprintf(`%s
%c`, html.EscapeString(f.Name()), '\n') } ss = `
` + ss + `
` dat["Html"] = template.HTML(ss) } w.Header().Set("Content-Type", "text/html; charset=utf-8") t.Execute(w, dat) return true }(w, r) { return } http.FileServer(http.Dir(db.SAVEPATH)).ServeHTTP(w, r) } } func (h *Handle) SearchHandler() { var q []string if h.r.Form.Get("to") != "" { q = append(q, fmt.Sprintf(` toaddr LIKE '%%%s%%'`, h.r.Form.Get("to"))) } if h.r.Form.Get("from") != "" { q = append(q, fmt.Sprintf(` fromaddr LIKE '%%%s%%'`, h.r.Form.Get("from"))) } if h.r.Form.Get("subject") != "" { q = append(q, fmt.Sprintf(` subject LIKE '%%%s%%'`, h.r.Form.Get("subject"))) } if h.r.Form.Get("date") != "" { q = append(q, fmt.Sprintf(` date LIKE '%%%s%%'`, h.r.Form.Get("date"))) } where := func() string { if len(q) == 0 { return "" } return strings.Join(q, " AND ") }() var err error if where != h.qu.GetWhere() { err = h.qu.SetWhere(where) if err != nil { TempErr(h.w, 500) return } } err = h.qu.SetPage(ParseInt(h.r.Form.Get("page")) - 1) if err != nil { TempErr(h.w, 500) return } if h.qu.TotalRows() == 0 { TempErr(h.w, 404) return } var htmlMetas []HtmlEM var from *mail.Address var to []*mail.Address var addrlist []string var htmlMeta HtmlEM for _, em := range h.qu.Rows() { htmlMeta = HtmlEM{ Id: em.Id, Date: db.TimeStr(em.Date), Subject: em.Subject, } if from, err = mail.ParseAddress(em.From); err != nil { htmlMeta.FromAddr = em.From } else { htmlMeta.FromName, htmlMeta.FromAddr = from.Name, from.Address } to, err = mail.ParseAddressList(em.To) if err != nil || len(to) == 0 { addrlist = strings.Split(em.To, ", ") if len(addrlist) == 0 { htmlMeta.ToAddr = em.To } else { htmlMeta.ToAddr = addrlist[0] } } else { htmlMeta.ToName, htmlMeta.ToAddr = to[0].Name, to[0].Address } htmlMetas = append(htmlMetas, htmlMeta) } dat := map[string]any{ "ToAddr": h.r.Form.Get("to"), "FromAddr": h.r.Form.Get("from"), "Subject": h.r.Form.Get("subject"), "Date": h.r.Form.Get("date"), "Page": h.qu.Page(), "TotalPages": h.qu.TotalPages(), "HtmlMetas": htmlMetas, } t.Execute(h.w, dat) }