]> Sergey Matveev's repositories - sgblog.git/blobdiff - cmd/sgblog/http.go
BLAKE3 instead of BLAKE2
[sgblog.git] / cmd / sgblog / http.go
index b05df3288682609bc4f20311f8be38500ffad9ac..fd5f7b448ec88bcdb7dc8a15d5b51e01a5575b5a 100644 (file)
@@ -1,6 +1,6 @@
 /*
-SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
-Copyright (C) 2020-2021 Sergey Matveev <stargrave@stargrave.org>
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
+Copyright (C) 2020-2023 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
@@ -21,6 +21,7 @@ import (
        "bytes"
        "compress/gzip"
        "crypto/sha1"
+       _ "embed"
        "encoding/hex"
        "encoding/xml"
        "errors"
@@ -29,140 +30,51 @@ import (
        "html"
        "io"
        "log"
+       "net/http"
        "net/url"
        "os"
        "strconv"
        "strings"
        "text/template"
+       "time"
 
        "github.com/go-git/go-git/v5"
        "github.com/go-git/go-git/v5/plumbing"
        "github.com/go-git/go-git/v5/plumbing/object"
+       "github.com/vorlif/spreak"
        "go.stargrave.org/sgblog"
        "go.stargrave.org/sgblog/cmd/sgblog/atom"
-       "golang.org/x/crypto/blake2b"
+       "lukechampine.com/blake3"
 )
 
 const (
        AtomPostsFeed    = "feed.atom"
        AtomCommentsFeed = "comments.atom"
-       TmplHTMLIndex    = `<html>
-<head>
-       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-       <meta name="generator" content="SGBlog {{.Version}}">
-       <title>{{.Cfg.Title}} {{if .Topic}}(topic: {{.Topic}}) {{end}}({{.Offset}}-{{.OffsetNext}})</title>
-       {{with .Cfg.CSS}}<link rel="stylesheet" type="text/css" href="{{.}}">{{end}}
-       {{with .Cfg.Webmaster}}<link rev="made" href="mailto:{{.}}">{{end}}
-       {{range .Cfg.GitURLs}}<link rel="vcs-git" href="{{.}}" title="Git repository">{{end}}
-       <link rel="top" href="{{.Cfg.URLPrefix}}/" title="top">
-       <link rel="alternate" title="Posts feed" href="{{.Cfg.AtomBaseURL}}{{.Cfg.URLPrefix}}/{{.AtomPostsFeed}}{{if .Topic}}?topic={{.Topic}}{{end}}" type="application/atom+xml">
-       {{if .CommentsEnabled}}<link rel="alternate" title="Comments feed" href="{{.Cfg.AtomBaseURL}}{{.Cfg.URLPrefix}}/{{.AtomCommentsFeed}}" type="application/atom+xml">{{end}}
-       {{if .Offset}}<link rel="prev" href="{{.Cfg.URLPrefix}}/?offset={{.OffsetPrev}}{{if .Topic}}&topic={{.Topic}}{{end}}" title="prev">{{end}}
-       {{if not .LogEnded}}<link rel="next" href="{{.Cfg.URLPrefix}}/?offset={{.OffsetNext}}{{if .Topic}}&topic={{.Topic}}{{end}}" title="next">{{end}}
-</head>
-<body>
-{{with .Cfg.AboutURL}}[<a href="{{.}}">about</a>]{{end}}
-{{block "links" .}}
-{{if .Offset}}[<a href="{{.Cfg.URLPrefix}}/?offset={{.OffsetPrev}}{{if .Topic}}&topic={{.Topic}}{{end}}">prev</a>]{{end}}
-{{if not .LogEnded}}[<a href="{{.Cfg.URLPrefix}}/?offset={{.OffsetNext}}{{if .Topic}}&topic={{.Topic}}{{end}}">next</a>]{{end}}
-{{end}}
-{{- $Cfg := .Cfg -}}
-{{if .Topics}}<hr/>
-Topics: [<tt><a href="{{$Cfg.URLPrefix}}/">ALL</a></tt>]
-{{range .Topics}}[<tt><a href="{{$Cfg.URLPrefix}}?topic={{.}}">{{.}}</a></tt>]
-{{end}}
-{{end}}
-{{- $TopicsEnabled := .TopicsEnabled -}}
-{{- $datePrev := "0001-01-01" -}}
-<table border=1>
-<tr>
-       <th>N</th><th>When</th><th>Title</th>
-       <th size="5%"><a title="Lines">L</a></th>
-       <th size="5%"><a title="Comments">C</a></th>
-       <th>Linked to</th>
-       {{if .TopicsEnabled}}<th>Topics</th>{{end}}
-</tr>
-{{range .Entries -}}
-{{- $dateCur := .Commit.Author.When.Format "2006-01-02" -}}
-{{- if ne $dateCur $datePrev -}}
-       <tr><td colspan={{if $TopicsEnabled}}7{{else}}7{{end}}><center><tt>{{$dateCur}}</tt></center></td></tr>
-       {{- $datePrev = $dateCur -}}
-{{- end -}}
-<tr>
-       <td>{{.Num}}</td>
-       <td><tt>{{.Commit.Author.When.Format "15:04"}}</tt></td>
-       <td><a href="{{$Cfg.URLPrefix}}/{{.Commit.Hash.String}}">{{.Title}}</a></td>
-       <td>{{.LinesNum}}</td>
-       <td>{{if .CommentsNum}}{{.CommentsNum}}{{else}}&nbsp;{{end}}</td>
-       <td>{{if .DomainURLs}}{{range .DomainURLs}} {{.}} {{end}}{{else}}&nbsp;{{end}}</td>
-       {{if $TopicsEnabled}}<td>{{if .Topics}}{{range .Topics}} <a href="{{$Cfg.URLPrefix}}/?topic={{.}}">{{.}}</a> {{end}}{{else}}&nbsp;{{end}}</td>{{end}}
-</tr>
-{{end}}</table>
-{{template "links" .}}
-</body>
-</html>
-`
-       TmplHTMLEntry = `{{$Cfg := .Cfg}}<html>
-<head>
-       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-       <meta name="generator" content="SGBlog {{.Version}}">
-       <title>{{.Title}} ({{.When}})</title>
-       {{with .Cfg.CSS}}<link rel="stylesheet" type="text/css" href="{{.}}">{{end}}
-       {{with .Cfg.Webmaster}}<link rev="made" href="mailto:{{.}}">{{end -}}
-       {{- range .Cfg.GitURLs}}
-       <link rel="vcs-git" href="{{.}}" title="Git repository">{{end}}
-       <link rel="top" href="{{.Cfg.URLPrefix}}/" title="top">
-       <link rel="alternate" title="Comments feed" href="{{.AtomCommentsURL}}" type="application/atom+xml">
-       {{if .Parent}}<link rel="prev" href="{{.Cfg.URLPrefix}}/{{.Parent}}" title="prev">{{end}}
-</head>
-<body>
-{{with .Cfg.AboutURL}}[<a href="{{.}}">about</a>]{{end}}
-[<a href="{{.Cfg.URLPrefix}}/">index</a>]
-{{if .Parent}}[<a href="{{.Cfg.URLPrefix}}/{{.Parent}}">prev</a>]{{end}}
-[<tt><a title="When">{{.When}}</a></tt>]
-[<tt><a title="What">{{.Commit.Hash.String}}</a></tt>]
-
-{{if .Topics}}
-<hr/>
-Topics: {{range .Topics}}[<tt><a href="{{$Cfg.URLPrefix}}?topic={{.}}">{{.}}</a></tt>]{{end}}
-{{end}}
-
-<hr/>
-<h2>{{.Title}}</h2>
-<pre>
-{{range .Lines}}{{. | lineURLize $Cfg.URLPrefix}}
-{{end}}</pre>
-<hr/>
-
-{{if .NoteLines}}Note:<pre>
-{{range .NoteLines}}{{. | lineURLize $Cfg.URLPrefix}}
-{{end}}</pre>
-<hr/>{{end}}
-
-{{if .Cfg.CommentsEmail}}[<a href="mailto:{{.Cfg.CommentsEmail}}?subject={{.TitleEscaped}}">leave comment</a>]{{end}}
-
-<dl>{{range $idx, $comment := .Comments}}
-<dt><a name="comment{{$idx}}"><a href="#comment{{$idx}}">comment {{$idx}}</a>:</dt>
-<dd><pre>
-{{range $comment.HeaderLines}}{{.}}
-{{end}}{{range $comment.BodyLines}}{{. | lineURLize $Cfg.URLPrefix}}
-{{end}}</pre></dd>
-{{end}}</dl>
-
-</body>
-</html>
-`
 )
 
 var (
        renderableSchemes = map[string]struct{}{
                "finger": {},
                "ftp":    {},
+               "gemini": {},
                "gopher": {},
                "http":   {},
                "https":  {},
+               "irc":    {},
+               "ircs":   {},
+               "news":   {},
                "telnet": {},
        }
+
+       //go:embed http-index.tmpl
+       TmplHTMLIndexRaw string
+       TmplHTMLIndex    = template.Must(template.New("http-index").Parse(TmplHTMLIndexRaw))
+
+       //go:embed http-entry.tmpl
+       TmplHTMLEntryRaw string
+       TmplHTMLEntry    = template.Must(template.New("http-entry").Funcs(
+               template.FuncMap{"lineURLize": lineURLizeInTemplate},
+       ).Parse(TmplHTMLEntryRaw))
 )
 
 type TableEntry struct {
@@ -217,7 +129,7 @@ func lineURLizeInTemplate(urlPrefix, line interface{}) string {
 
 func startHeader(etag hash.Hash, gziped bool) string {
        lines := []string{
-               "Content-Type: text/html; charset=UTF-8",
+               "Content-Type: text/html; charset=utf-8",
                "ETag: " + etagString(etag),
        }
        if gziped {
@@ -228,10 +140,11 @@ func startHeader(etag hash.Hash, gziped bool) string {
        return strings.Join(lines, "\n")
 }
 
-func makeErr(err error) {
-       fmt.Print("Content-Type: text/plain; charset=UTF-8\n\n")
+func makeErr(err error, status int) {
+       fmt.Println("Status:", status)
+       fmt.Print("Content-Type: text/plain; charset=utf-8\n\n")
        fmt.Println(err)
-       panic(err)
+       log.Fatalln(err)
 }
 
 func checkETag(etag hash.Hash) {
@@ -262,20 +175,18 @@ func serveHTTP() {
        if err != nil {
                log.Fatalln(err)
        }
+       initLocalizer(cfg.Lang)
 
-       pathInfo, exists := os.LookupEnv("PATH_INFO")
-       if !exists {
+       pathInfo := os.Getenv("PATH_INFO")
+       if len(pathInfo) == 0 {
                pathInfo = "/"
        }
        queryValues, err := url.ParseQuery(os.Getenv("QUERY_STRING"))
        if err != nil {
-               makeErr(err)
+               makeErr(err, http.StatusBadRequest)
        }
 
-       etagHash, err := blake2b.New256(nil)
-       if err != nil {
-               panic(err)
-       }
+       etagHash := blake3.New(32, nil)
        for _, s := range []string{
                "SGBLOG",
                sgblog.Version,
@@ -298,13 +209,11 @@ func serveHTTP() {
                cfg.CommentsNotesRef,
                cfg.CommentsEmail,
        }
-       for _, gitURL := range cfg.GitURLs {
-               etagHashForWeb = append(etagHashForWeb, gitURL)
-       }
+       etagHashForWeb = append(etagHashForWeb, cfg.GitURLs...)
 
        headHash, err := initRepo(cfg)
        if err != nil {
-               makeErr(err)
+               makeErr(err, http.StatusInternalServerError)
        }
 
        if notes, err := repo.Notes(); err == nil {
@@ -348,20 +257,20 @@ func serveHTTP() {
                if offsetRaw, exists := queryValues["offset"]; exists {
                        offset, err = strconv.Atoi(offsetRaw[0])
                        if err != nil {
-                               makeErr(err)
+                               makeErr(err, http.StatusBadRequest)
                        }
                }
                repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
                topicsCache, err := getTopicsCache(cfg, repoLog)
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
                repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
 
                commitN := 0
@@ -371,7 +280,7 @@ func serveHTTP() {
                        topic = t[0]
                        hashes := topicsCache[topic]
                        if hashes == nil {
-                               makeErr(errors.New("no posts with that topic"))
+                               makeErr(errors.New("no posts with that topic"), http.StatusBadRequest)
                        }
                        if len(hashes) > offset {
                                hashes = hashes[offset:]
@@ -435,9 +344,9 @@ func serveHTTP() {
                if offsetPrev < 0 {
                        offsetPrev = 0
                }
-               tmpl := template.Must(template.New("index").Parse(TmplHTMLIndex))
                os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
-               err = tmpl.Execute(out, struct {
+               err = TmplHTMLIndex.Execute(out, struct {
+                       T                *spreak.Localizer
                        Version          string
                        Cfg              *Cfg
                        Topic            string
@@ -452,6 +361,7 @@ func serveHTTP() {
                        LogEnded         bool
                        Entries          []TableEntry
                }{
+                       T:                localizer,
                        Version:          sgblog.Version,
                        Cfg:              cfg,
                        Topic:            topic,
@@ -467,12 +377,44 @@ func serveHTTP() {
                        Entries:          entries,
                })
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
+               }
+       } else if pathInfo == "/twtxt.txt" {
+               commit, err := repo.CommitObject(*headHash)
+               if err != nil {
+                       makeErr(err, http.StatusInternalServerError)
+               }
+               etagHash.Write([]byte("TWTXT POSTS"))
+               etagHash.Write(commit.Hash[:])
+               checkETag(etagHash)
+               repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
+               if err != nil {
+                       makeErr(err, http.StatusInternalServerError)
                }
+               for i := 0; i < PageEntries; i++ {
+                       commit, err = repoLog.Next()
+                       if err != nil {
+                               break
+                       }
+                       fmt.Fprintf(
+                               out, "%s\t%s\n",
+                               commit.Author.When.Format(time.RFC3339),
+                               msgSplit(commit.Message)[0],
+                       )
+               }
+               os.Stdout.WriteString("Content-Type: text/plain; charset=utf-8\n")
+               os.Stdout.WriteString("ETag: " + etagString(etagHash) + "\n")
+               if gzipWriter != nil {
+                       os.Stdout.WriteString("Content-Encoding: gzip\n")
+                       gzipWriter.Close()
+               }
+               os.Stdout.WriteString("\n")
+               os.Stdout.Write(outBuf.Bytes())
+               return
        } else if pathInfo == "/"+AtomPostsFeed {
                commit, err := repo.CommitObject(*headHash)
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
 
                var topic string
@@ -490,10 +432,7 @@ func serveHTTP() {
                } else {
                        title = fmt.Sprintf("%s (topic: %s)", cfg.Title, topic)
                }
-               idHasher, err := blake2b.New256(nil)
-               if err != nil {
-                       panic(err)
-               }
+               idHasher := blake3.New(32, nil)
                idHasher.Write([]byte("ATOM POSTS"))
                idHasher.Write([]byte(cfg.AtomId))
                idHasher.Write([]byte(topic))
@@ -510,7 +449,7 @@ func serveHTTP() {
 
                repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
                var commits CommitIterNext
                if topic == "" {
@@ -518,11 +457,11 @@ func serveHTTP() {
                } else {
                        topicsCache, err := getTopicsCache(cfg, repoLog)
                        if err != nil {
-                               makeErr(err)
+                               makeErr(err, http.StatusInternalServerError)
                        }
                        hashes := topicsCache[topic]
                        if hashes == nil {
-                               makeErr(errors.New("no posts with that topic"))
+                               makeErr(errors.New("no posts with that topic"), http.StatusBadRequest)
                        }
                        commits = &HashesIter{hashes}
                }
@@ -534,7 +473,9 @@ func serveHTTP() {
                        }
                        lines := msgSplit(commit.Message)
                        var categories []atom.Category
-                       for _, topic := range sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)) {
+                       for _, topic := range sgblog.ParseTopics(sgblog.GetNote(
+                               repo, topicsTree, commit.Hash,
+                       )) {
                                categories = append(categories, atom.Category{Term: topic})
                        }
                        htmlized := make([]string, 0, len(lines))
@@ -562,22 +503,20 @@ func serveHTTP() {
                }
                data, err := xml.MarshalIndent(&feed, "", "  ")
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
+               out.Write([]byte(xml.Header))
                out.Write(data)
                goto AtomFinish
        } else if pathInfo == "/"+AtomCommentsFeed {
                commit, err := repo.CommitObject(commentsRef.Hash())
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
                etagHash.Write([]byte("ATOM COMMENTS"))
                etagHash.Write(commit.Hash[:])
                checkETag(etagHash)
-               idHasher, err := blake2b.New256(nil)
-               if err != nil {
-                       panic(err)
-               }
+               idHasher := blake3.New(32, nil)
                idHasher.Write([]byte("ATOM COMMENTS"))
                idHasher.Write([]byte(cfg.AtomId))
                feed := atom.Feed{
@@ -592,7 +531,7 @@ func serveHTTP() {
                }
                repoLog, err := repo.Log(&git.LogOptions{From: commentsRef.Hash()})
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
                for i := 0; i < PageEntries; i++ {
                        commit, err = repoLog.Next()
@@ -601,11 +540,11 @@ func serveHTTP() {
                        }
                        fileStats, err := commit.Stats()
                        if err != nil {
-                               makeErr(err)
+                               makeErr(err, http.StatusInternalServerError)
                        }
                        t, err := commit.Tree()
                        if err != nil {
-                               makeErr(err)
+                               makeErr(err, http.StatusInternalServerError)
                        }
                        commentedHash := plumbing.NewHash(strings.ReplaceAll(
                                fileStats[0].Name, "/", "",
@@ -656,14 +595,15 @@ func serveHTTP() {
                }
                data, err := xml.MarshalIndent(&feed, "", "  ")
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
+               out.Write([]byte(xml.Header))
                out.Write(data)
                goto AtomFinish
        } else if sha1DigestRe.MatchString(pathInfo[1:]) {
                commit, err := repo.CommitObject(plumbing.NewHash(pathInfo[1 : 1+sha1.Size*2]))
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusBadRequest)
                }
                for _, data := range etagHashForWeb {
                        etagHash.Write([]byte(data))
@@ -704,10 +644,7 @@ func serveHTTP() {
                                        body: lines[3:],
                                })
                        }
-                       idHasher, err := blake2b.New256(nil)
-                       if err != nil {
-                               panic(err)
-                       }
+                       idHasher := blake3.New(32, nil)
                        idHasher.Write([]byte("ATOM COMMENTS"))
                        idHasher.Write(commit.Hash[:])
                        feed := atom.Feed{
@@ -762,8 +699,9 @@ func serveHTTP() {
                        }
                        data, err := xml.MarshalIndent(&feed, "", "  ")
                        if err != nil {
-                               makeErr(err)
+                               makeErr(err, http.StatusInternalServerError)
                        }
+                       out.Write([]byte(xml.Header))
                        out.Write(data)
                        goto AtomFinish
                }
@@ -789,11 +727,9 @@ func serveHTTP() {
                        notesLines = strings.Split(string(notesRaw), "\n")
                }
 
-               tmpl := template.New("entry")
-               tmpl = tmpl.Funcs(template.FuncMap{"lineURLize": lineURLizeInTemplate})
-               tmpl = template.Must(tmpl.Parse(TmplHTMLEntry))
                os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
-               err = tmpl.Execute(out, struct {
+               err = TmplHTMLEntry.Execute(out, struct {
+                       T               *spreak.Localizer
                        Version         string
                        Cfg             *Cfg
                        Title           string
@@ -807,6 +743,7 @@ func serveHTTP() {
                        Comments        []CommentEntry
                        Topics          []string
                }{
+                       T:               localizer,
                        Version:         sgblog.Version,
                        Cfg:             cfg,
                        Title:           title,
@@ -821,10 +758,10 @@ func serveHTTP() {
                        Topics:          sgblog.ParseTopics(topicsRaw),
                })
                if err != nil {
-                       makeErr(err)
+                       makeErr(err, http.StatusInternalServerError)
                }
        } else {
-               makeErr(errors.New("unknown URL action"))
+               makeErr(errors.New("unknown URL action"), http.StatusNotFound)
        }
        out.Write([]byte("</body></html>\n"))
        if gzipWriter != nil {
@@ -834,7 +771,7 @@ func serveHTTP() {
        return
 
 AtomFinish:
-       os.Stdout.WriteString("Content-Type: application/atom+xml; charset=UTF-8\n")
+       os.Stdout.WriteString("Content-Type: application/atom+xml; charset=utf-8\n")
        os.Stdout.WriteString("ETag: " + etagString(etagHash) + "\n")
        if gzipWriter != nil {
                os.Stdout.WriteString("Content-Encoding: gzip\n")