package main import "embed" import "fmt" import "html/template" import "log" import "net/http" import "net/url" import "reflect" import "strconv" import "strings" import "time" const dashLinks = template.HTML( "
entries • " + "import • " + "export
" ) //go:embed template/* var templateFS embed.FS var Templates *template.Template func init() { var err error Templates, err = template.ParseFS(templateFS, "template/*.html") if err != nil { panic(err) } } func Req2Date(r *http.Request) (date Date, ok bool) { year, err := strconv.ParseUint(r.PathValue("year"), 10, 32) if err != nil { return date, false } month, err := strconv.ParseUint(r.PathValue("month"), 10, 32) if err != nil { return date, false } day, err := strconv.ParseUint(r.PathValue("day"), 10, 32) if err != nil { return date, false } date = Time2Date(time.Date(int(year), time.Month(month), int(day), 0, 0, 0, 0, time.UTC)) if date.Year != int(year) || date.Month != int(month) || date.Day != int(day) { return date, false } return date, true } func NotImplemented(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not Implemented!", 400) } func TestSecHeaders(r *http.Request) bool { sfm := r.Header.Get("Sec-Fetch-Mode") if sfm == "websocket" { return false } else if sfm == "cors" || sfm == "no-cors" { sfs := r.Header.Get("Sec-Fetch-Site") return sfs != "cross-site" } else { return true } } func Login(r *http.Request) (jrnl *Jrnl, user string) { if !TestSecHeaders(r) { return nil, "" } else if r.Host != "" && r.Method == "POST" { origin := r.Header.Get("Origin") if origin == "" { origin = r.Header.Get("Referer") } if origin != "" { uri, err := url.Parse(origin) if err != nil && uri.Host != r.Host { return nil, "" } } } user, pass, ok := r.BasicAuth() if !ok { return nil, "" } jrnl, ok = Jrnls[user] if !ok { return nil, user } if !jrnl.Passhash.Check(pass) { return nil, user } else { return jrnl, user } } func MustLogin(w http.ResponseWriter, r *http.Request) *Jrnl { jrnl, user := Login(r) if jrnl == nil { if user != "" { log.Printf("failed login attempt as %s from %s", user, r.RemoteAddr) } w.Header().Add("WWW-Authenticate", "Basic realm=\"jrnl\"") http.Error(w, "Unauthorized!", 401) return nil } else { return jrnl } } type Err500Page struct { Links template.HTML Title string Type reflect.Type Error error } func Err500(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, err error) { ty := reflect.TypeOf(err) log.Printf("error 500 serving %s for %s: %s %s", r.URL.Path, jrnl.Id, ty, err) w.WriteHeader(500) ReplyTemplate( w, r, jrnl, "500.html", Err500Page { Links: BuildLinks(r, jrnl), Title: "Jrnl Server Error", Type: ty, Error: err } ) } func NotFoundGet(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } NotFoundPage(w, r, jrnl) } type NotFoundPageData struct { Links template.HTML Title string } func NotFoundPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl) { w.WriteHeader(404) ReplyTemplate( w, r, jrnl, "404.html", NotFoundPageData { Links: BuildLinks(r, jrnl), Title: "Jrnl Page Not Found" } ) } func ReplyTemplate(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, tpl string, ctx interface{}) { var buf strings.Builder err := Templates.ExecuteTemplate(&buf, tpl, ctx) if err != nil { msg := fmt.Sprintf("error in template %s: %s", tpl, err) log.Println(msg) Err500(w, r, jrnl, err) } else { w.Write([]byte(buf.String())) } } func Redirect(loc string, code int) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } http.Redirect(w, r, loc, code) } } func BuildLinks(r *http.Request, jrnl *Jrnl) template.HTML { if jrnl == nil { return template.HTML("") } pfx := strings.Repeat("../", strings.Count(r.URL.Path, "/") - 1) if pfx == "" { pfx = "./" } links := fmt.Sprintf("dash", pfx) years, _ := jrnl.ListYears() for i, year := range years { if i < 5 { links += " • " links += fmt.Sprintf( "jrnl/%04d", pfx, year, year ) } else { break } } return template.HTML("" + links + "
") } func NtriesGet(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } ents, err := jrnl.ListEntries() if err != nil { Err500(w, r, jrnl, err) return } vers := make([]EntryVersion, len(ents)) for i, e := range ents { n, err := e.GetLatest() if err != nil { Err500(w, r, jrnl, err) return } v, err := e.GetVersion(n) if err != nil { Err500(w, r, jrnl, err) return } vers[i] = v } NtriesPage( w, r, jrnl, "Jrnl Entry Index", "This is a log of every jrnl entry you've made.", vers ) } type NtriesPageData struct { Links template.HTML Title, Blurb string Ntries []EntryVersion Pfx string } func NtriesPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, Title, Blurb string, ntries []EntryVersion) { pfx := strings.Repeat("../", strings.Count(r.URL.Path, "/") - 1) ReplyTemplate( w, r, jrnl, "ntries.html", NtriesPageData { Links: BuildLinks(r, jrnl), Title: Title, Blurb: Blurb, Ntries: ntries, Pfx: pfx } ) } type DashPage struct { Links template.HTML Title string Jrnl *Jrnl Today, Yesterday Date } func Dash(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } today := time.Now().In((*time.Location)(jrnl.Timezone)) yesterday := today.AddDate(0,0,-1) ReplyTemplate( w, r, jrnl, "dash.html", DashPage { Links: dashLinks, Title: "Jrnl Dash", Jrnl: jrnl, Today: Time2Date(today), Yesterday: Time2Date(yesterday), } ) } func GotoRedirect(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } query := r.URL.Query() pfx, _ := query["pfx"] dest, _ := query["dest"] path := strings.Join(pfx, "") + strings.Join(dest, "") http.Redirect(w, r, path, 301) } func JrnlRedirect(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } years, err := jrnl.ListYears() if err != nil { http.Error(w, err.Error(), 500) } else if len(years) > 0 { http.Redirect(w, r, fmt.Sprintf("%04d", years[0]), 302) } else { http.Redirect(w, r, "../init", 302) } } func JrnlGet(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } year, err := strconv.ParseUint(r.PathValue("year"), 10, 32) if err != nil { NotFoundPage(w, r, jrnl) return } JrnlPage(w, r, jrnl, int(year)) } func JrnlPost(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } vers, err := jrnl.PutText(r.FormValue("text")) if err != nil { Err500(w, r, jrnl, err) return } urlYear, err := strconv.ParseUint(r.PathValue("year"), 10, 32) if err != nil { NotFoundPage(w, r, jrnl) return } if len(vers) == 0 || (len(vers) == 1 && vers[0].Entry.Date.Year == int(urlYear)) { JrnlPage(w, r, jrnl, int(urlYear)) } else if len(vers) == 1 { http.Redirect(w, r, fmt.Sprintf("%04d", vers[0].Entry.Date.Year), 302) } else { NtriesPage( w, r, jrnl, "Jrnl Update", "The following entries were changed:", vers ) } } type JrnlPageData struct { Links template.HTML Title string Jrnl *Jrnl Year int } func JrnlPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, year int) { ReplyTemplate( w, r, jrnl, "jrnl.html", JrnlPageData { Links: BuildLinks(r, jrnl), Title: fmt.Sprintf("Jrnl for Year %04d", year), Jrnl: jrnl, Year: year} ) } type InitPageData struct { Links template.HTML Title string } func InitGet(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } ReplyTemplate( w, r, jrnl, "init.html", InitPageData { BuildLinks(r, jrnl), "Jrnl Import" } ) } func InitPost(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } vers, err := jrnl.PutText(r.FormValue("text")) if err != nil { Err500(w, r, jrnl, err) return } var blurb string if len(vers) < 1 { blurb = "No entries were imported." } else if len(vers) == 1 { blurb = "The following entry was imported:" } else { blurb = "The following entries were imported:" } NtriesPage(w, r, jrnl, "Jrnl Import", blurb, vers) } type SavePageData struct { Links template.HTML Title string Jrnl *Jrnl } func SaveGet(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } query := r.URL.Query() if _, ok := query["download"]; ok { backup, err := jrnl.GetBackup() if err != nil { Err500(w, r, jrnl, err) return } w.Header().Add("Content-Type", "text/plain; charset=UTF-8") w.Header().Add("Content-Disposition", "attachment; filename=\"backup.txt\"") w.Write([]byte(backup)) } else { ReplyTemplate( w, r, jrnl, "save.html", SavePageData { Links: BuildLinks(r, jrnl), Title: "Jrnl Export", Jrnl: jrnl } ) } } func NtryGet(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } date, ok := Req2Date(r) if !ok { NotFoundPage(w, r, jrnl) return } NtryPage(w, r, jrnl, date) } func NtryPost(w http.ResponseWriter, r *http.Request) { jrnl := MustLogin(w, r) if jrnl == nil { return } date, ok := Req2Date(r) if !ok { NotFoundPage(w, r, jrnl) return } _, _, err := jrnl.PutEntry(date, r.FormValue("text")) if err != nil { Err500(w, r, jrnl, err) return } NtryPage(w, r, jrnl, date) } type NtryPageData struct { Links template.HTML Title string Entry *Entry } func NtryPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, date Date) { ent, err := jrnl.GetEntry(date) if err == NotFound { NotFoundPage(w, r, jrnl) return } else if err != nil { Err500(w, r, jrnl, err) return } ReplyTemplate( w, r, jrnl, "ntry.html", NtryPageData { Links: BuildLinks(r, jrnl), Title: fmt.Sprintf("Jrnl Entry for %s", date), Entry: &ent } ) }