From: Sergey Matveev Date: Tue, 14 Sep 2021 19:28:55 +0000 (+0300) Subject: Experimental gemini support X-Git-Tag: v0.21.0^0 X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=fae0cb894b0c46bbb438b2ec50ad2b3f05c24253;hp=bf2fdfb0385c310d0997998945015dc67d6cf3fc;p=sgblog.git Experimental gemini support --- diff --git a/README.texi b/README.texi index adc6073..9409b09 100644 --- a/README.texi +++ b/README.texi @@ -9,15 +9,16 @@ Copyright @copyright{} 2020 @email{stargrave@@stargrave.org, Sergey Matveev} @node Top @top SGBlog -SGBlog is minimalistic and simple Git-backed CGI/inetd -@url{https://en.wikipedia.org/wiki/Blog, blogging} and -@url{https://en.wikipedia.org/wiki/Phlog, phlogging} engine +SGBlog is minimalistic and simple Git-backed CGI/UCSPI +@url{https://en.wikipedia.org/wiki/Blog, blogging} (@code{http://}/@code{https://}), +@url{https://en.wikipedia.org/wiki/Phlog, phlogging} (@code{gopher://}) and +gemlogging (@code{gemini://}) engine with email-backed comments support, written on @url{https://golang.org/, Go}. Its main competitive features: @itemize -@item Single binary, responsible for both blog and phlog +@item Single binary, responsible for both blog, phlog and gemlog @item @url{https://git-scm.com/, Git} DVCS as a storage for posts and comments @item Single small @url{https://hjson.github.io/, Hjson} configuration file @item Uses @url{https://en.wikipedia.org/wiki/Common_Gateway_Interface, CGI} @@ -32,10 +33,10 @@ Its main competitive features: for posts, comments and per-post comments @item Single binary for email-backed comments posting @item If access is granted, then everyone can easily create an offline - copy of your blog/phlog! + copy of your blog/phlog/gemlog! @end itemize -All of that, except for comments, topics and phlog, could be achieved +All of that, except for comments, topics and phlog/gemlog, could be achieved with some Git viewer like @url{https://git.zx2c4.com/cgit/about/, cgit}. But SGBlog also is able to: @@ -183,6 +184,15 @@ EOF # mv /var/service/.phlog-ipv6 /var/service/phlog-ipv6 @end example +Gemlog uses Gemini protocol that requires TLS usage, that can be +achieved with @url{go.cypherpunks.ru/ucspi} tools: + +@example +exec tcpserver -DRH -u $uid -g $gid -l 0 ::0 1965 \ + tlss -key gemlog.key.pem -cert gemlog.pem \ + sgblog -gemini /home/sgblog/gemlog.hjson 2>&1 +@end example + For comments workability you have to configure your SMTP server to feed incoming messages to @command{sgblog-comment-add} utility. For example, Postfix'es @file{/etc/aliases} can contain: diff --git a/cmd/sgblog-comment-add/mail.go b/cmd/sgblog-comment-add/mail.go index 7b804c1..3ffc1e8 100644 --- a/cmd/sgblog-comment-add/mail.go +++ b/cmd/sgblog-comment-add/mail.go @@ -1,5 +1,5 @@ /* -SGBlog -- Git-backed CGI/inetd blogging/phlogging engine +SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2021 Sergey Matveev This program is free software: you can redistribute it and/or modify diff --git a/cmd/sgblog-comment-add/main.go b/cmd/sgblog-comment-add/main.go index e632ca6..a1e7207 100644 --- a/cmd/sgblog-comment-add/main.go +++ b/cmd/sgblog-comment-add/main.go @@ -1,5 +1,5 @@ /* -SGBlog -- Git-backed CGI/inetd blogging/phlogging engine +SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2021 Sergey Matveev This program is free software: you can redistribute it and/or modify @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -// Git-backed CGI/inetd blogging/phlogging engine +// Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine package main import ( diff --git a/cmd/sgblog-topics/main.go b/cmd/sgblog-topics/main.go index 121380f..f936461 100644 --- a/cmd/sgblog-topics/main.go +++ b/cmd/sgblog-topics/main.go @@ -1,5 +1,5 @@ /* -SGBlog -- Git-backed CGI/inetd blogging/phlogging engine +SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2021 Sergey Matveev This program is free software: you can redistribute it and/or modify @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -// Git-backed CGI/inetd blogging/phlogging engine +// Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine package main import ( diff --git a/cmd/sgblog/gemini.go b/cmd/sgblog/gemini.go new file mode 100644 index 0000000..af95e67 --- /dev/null +++ b/cmd/sgblog/gemini.go @@ -0,0 +1,241 @@ +/* +SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine +Copyright (C) 2020-2021 Sergey Matveev + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "log" + "net/url" + "os" + "strconv" + "text/template" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "go.stargrave.org/sgblog" +) + +const ( + TmplGemMenu = `{{$CR := printf "\r"}}20 text/gemini{{$CR}} +# {{.Cfg.Title}} {{if .Topic}}(topic: {{.Topic}}) {{end}}({{.Offset}}-{{.OffsetNext}}) +{{if .Cfg.AboutURL}}=> {{.Cfg.AboutURL}} About{{end}} +{{if .Offset}}=> /?offset={{.OffsetPrev}}{{if .Topic}}&topic={{.Topic}}{{end}} Prev{{end}} +{{if not .LogEnded}}=> /?offset={{.OffsetNext}}{{if .Topic}}&topic={{.Topic}}{{end}} Next{{end}} + +{{$datePrev := "0001-01-01" -}} +{{- range .Entries -}} +{{- $dateCur := .Commit.Author.When.Format "2006-01-02" -}} +{{- if ne $dateCur $datePrev}}{{$datePrev = $dateCur}}## {{$dateCur}} +{{end -}} +=> /{{.Commit.Hash.String}} [{{.Commit.Author.When.Format "15:04"}}] {{.Title}} ({{.LinesNum}}L){{with .CommentsNum}} ({{.}}C){{end}}{{if .Topics}}{{range .Topics}} {{.}}{{end}}{{end}} +{{end}} + +{{range .Topics -}}=> /?topic={{.}} Topic: {{.}} +{{end}} +Generated by: SGBlog {{.Version}} +` + TmplGemEntry = `{{$CR := printf "\r"}}20 text/gemini{{$CR}} +# {{.Title}} +What: {{.Commit.Hash.String}} +When: {{.When}} +{{if .Topics}}Topics:{{range .Topics}} {{.}}{{end}}{{end}} +` + "```" + ` +{{.Commit.Message}}` + "```" + ` +{{- if .Note}} +## Note: +` + "```" + ` +{{.Note}} +` + "```" + ` +{{end -}} +{{- if .Cfg.CommentsEmail}} +=> mailto:{{.Cfg.CommentsEmail}}?subject={{.TitleEscaped}} leave comment +{{end}}{{range $idx, $comment := .Comments}} +## comment {{$idx}}: +` + "```" + ` +{{$comment}} +` + "```" + ` +{{end}} +Generated by: SGBlog {{.Version}} +` +) + +func makeGemErr(err error) { + fmt.Print("59 " + err.Error() + "\r\n") + log.Fatalln(err) +} + +func serveGemini(cfgPath string) { + cfg, err := readCfg(cfgPath) + if err != nil { + log.Fatalln(err) + } + + headHash, err := initRepo(cfg) + if err != nil { + log.Fatalln(err) + } + + scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 2+1<<10)) + if !scanner.Scan() { + makeGemErr(errors.New("no CRLF found")) + } + urlRaw := scanner.Text() + u, err := url.Parse(urlRaw) + if err != nil { + makeGemErr(err) + } + if u.Scheme != "gemini" { + makeGemErr(errors.New("only gemini:// is supported" + u.String())) + } + + if u.Path == "/" { + offset := 0 + if offsetRaw, exists := u.Query()["offset"]; exists { + offset, err = strconv.Atoi(offsetRaw[0]) + if err != nil { + makeGemErr(err) + } + } + repoLog, err := repo.Log(&git.LogOptions{From: *headHash}) + if err != nil { + makeGemErr(err) + } + topicsCache, err := getTopicsCache(cfg, repoLog) + if err != nil { + makeGemErr(err) + } + repoLog, err = repo.Log(&git.LogOptions{From: *headHash}) + if err != nil { + makeGemErr(err) + } + + commitN := 0 + var commits CommitIterNext + var topic string + if t, exists := u.Query()["topic"]; exists { + topic = t[0] + hashes := topicsCache[topic] + if hashes == nil { + makeGemErr(errors.New("no posts with that topic")) + } + if len(hashes) > offset { + hashes = hashes[offset:] + commitN += offset + } + commits = &HashesIter{hashes} + } else { + for i := 0; i < offset; i++ { + if _, err = repoLog.Next(); err != nil { + break + } + commitN++ + } + commits = repoLog + } + + logEnded := false + entries := make([]TableMenuEntry, 0, PageEntries) + for i := 0; i < PageEntries; i++ { + commit, err := commits.Next() + if err != nil { + logEnded = true + break + } + lines := msgSplit(commit.Message) + entries = append(entries, TableMenuEntry{ + Commit: commit, + Title: lines[0], + LinesNum: len(lines) - 2, + CommentsNum: len(sgblog.ParseComments(sgblog.GetNote( + repo, commentsTree, commit.Hash, + ))), + Topics: sgblog.ParseTopics(sgblog.GetNote( + repo, topicsTree, commit.Hash, + )), + }) + } + tmpl := template.Must(template.New("menu").Parse(TmplGemMenu)) + offsetPrev := offset - PageEntries + if offsetPrev < 0 { + offsetPrev = 0 + } + err = tmpl.Execute(os.Stdout, struct { + Cfg *Cfg + Topic string + Offset int + OffsetPrev int + OffsetNext int + LogEnded bool + Entries []TableMenuEntry + Topics []string + Version string + }{ + Cfg: cfg, + Topic: topic, + Offset: offset, + OffsetPrev: offsetPrev, + OffsetNext: offset + PageEntries, + LogEnded: logEnded, + Entries: entries, + Topics: topicsCache.Topics(), + Version: sgblog.Version, + }) + if err != nil { + log.Fatalln(err) + } + } else if sha1DigestRe.MatchString(u.Path[1:]) { + commit, err := repo.CommitObject(plumbing.NewHash(u.Path[1:])) + if err != nil { + log.Fatalln(err) + } + tmpl := template.Must(template.New("entry").Parse(TmplGemEntry)) + title := msgSplit(commit.Message)[0] + err = tmpl.Execute(os.Stdout, struct { + Title string + Commit *object.Commit + When string + Cfg *Cfg + Note string + Comments []string + Topics []string + Version string + TitleEscaped string + }{ + Title: title, + Commit: commit, + When: commit.Author.When.Format(sgblog.WhenFmt), + Cfg: cfg, + Note: string(sgblog.GetNote(repo, notesTree, commit.Hash)), + Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)), + Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)), + Version: sgblog.Version, + TitleEscaped: url.PathEscape(fmt.Sprintf( + "Re: %s (%s)", title, commit.Hash, + )), + }) + if err != nil { + log.Fatalln(err) + } + } else { + makeGemErr(errors.New("unknown URL action")) + } +} diff --git a/cmd/sgblog/gopher.go b/cmd/sgblog/gopher.go index 78c0987..27ad884 100644 --- a/cmd/sgblog/gopher.go +++ b/cmd/sgblog/gopher.go @@ -1,5 +1,5 @@ /* -SGBlog -- Git-backed CGI/inetd blogging/phlogging engine +SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2021 Sergey Matveev This program is free software: you can redistribute it and/or modify diff --git a/cmd/sgblog/http.go b/cmd/sgblog/http.go index 9ee50ce..f1e7404 100644 --- a/cmd/sgblog/http.go +++ b/cmd/sgblog/http.go @@ -1,5 +1,5 @@ /* -SGBlog -- Git-backed CGI/inetd blogging/phlogging engine +SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2021 Sergey Matveev This program is free software: you can redistribute it and/or modify diff --git a/cmd/sgblog/main.go b/cmd/sgblog/main.go index ef3ff45..8dbb1d8 100644 --- a/cmd/sgblog/main.go +++ b/cmd/sgblog/main.go @@ -1,5 +1,5 @@ /* -SGBlog -- Git-backed CGI/inetd blogging/phlogging engine +SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2021 Sergey Matveev This program is free software: you can redistribute it and/or modify @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -// Git-backed CGI/inetd blogging/phlogging engine +// Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine package main import ( @@ -24,6 +24,7 @@ import ( "flag" "fmt" "io/ioutil" + "log" "regexp" "strings" @@ -146,15 +147,20 @@ func readCfg(cfgPath string) (*Cfg, error) { func main() { gopherCfgPath := flag.String("gopher", "", "Path to gopher-related configuration file") + geminiCfgPath := flag.String("gemini", "", "Path to gemini-related configuration file") flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), `Usage of sgblog: sgblog -- run CGI HTTP backend sgblog -gopher /path/to/cfg.hjson -- run UCSPI/inetd Gopher backend + sgblog -gemini /path/to/cfg.hjson -- run UCSPI+tlss Gemini backend `) } flag.Parse() + log.SetFlags(log.Lshortfile) if *gopherCfgPath != "" { serveGopher(*gopherCfgPath) + } else if *geminiCfgPath != "" { + serveGemini(*geminiCfgPath) } else { serveHTTP() } diff --git a/cmd/sgblog/topics.go b/cmd/sgblog/topics.go index 2ab6328..9a89793 100644 --- a/cmd/sgblog/topics.go +++ b/cmd/sgblog/topics.go @@ -1,5 +1,5 @@ /* -SGBlog -- Git-backed CGI/inetd blogging/phlogging engine +SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine Copyright (C) 2020-2021 Sergey Matveev This program is free software: you can redistribute it and/or modify diff --git a/common.go b/common.go index 90adb7a..4e0179f 100644 --- a/common.go +++ b/common.go @@ -1,4 +1,4 @@ -// SGBlog -- Git-backed CGI/inetd blogging/phlogging engine +// SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine package sgblog import ( @@ -15,7 +15,7 @@ import ( ) const ( - Version = "0.20.2" + Version = "0.21.0" WhenFmt = "2006-01-02 15:04:05Z07:00" )