Initial commit
This commit is contained in:
commit
db05f5b473
|
@ -0,0 +1 @@
|
||||||
|
konbata
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 blank X
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,5 @@
|
||||||
|
module konbata
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require git.sr.ht/~adnano/go-gemini v0.2.2 // indirect
|
|
@ -0,0 +1,9 @@
|
||||||
|
git.sr.ht/~adnano/go-gemini v0.2.2 h1:p2owKzrQ1wTgvPS5CZCPYArQyNUL8ZgYOHHrTjH9sdI=
|
||||||
|
git.sr.ht/~adnano/go-gemini v0.2.2/go.mod h1:hQ75Y0i5jSFL+FQ7AzWVAYr5LQsaFC7v3ZviNyj46dY=
|
||||||
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||||
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
@ -0,0 +1,227 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
"errors"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"net/url"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/xml"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.sr.ht/~adnano/go-gemini"
|
||||||
|
"git.sr.ht/~adnano/go-gemini/tofu"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hosts tofu.KnownHosts
|
||||||
|
hostsfile *tofu.HostWriter
|
||||||
|
)
|
||||||
|
|
||||||
|
func xdgDataHome() string {
|
||||||
|
if s, ok := os.LookupEnv("XDG_DATA_HOME"); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return filepath.Join(os.Getenv("HOME"), ".local", "share")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
path := filepath.Join(xdgDataHome(), "konbata", "known_hosts")
|
||||||
|
err := hosts.Load(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostsfile, err = tofu.OpenHostsFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func trustCertificate(hostname string, cert *x509.Certificate) error {
|
||||||
|
host := tofu.NewHost(hostname, cert.Raw)
|
||||||
|
knownHost, ok := hosts.Lookup(hostname)
|
||||||
|
if ok {
|
||||||
|
// Check fingerprint
|
||||||
|
if knownHost.Fingerprint != host.Fingerprint {
|
||||||
|
return errors.New("fingerprint does not match!")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts.Add(host)
|
||||||
|
hostsfile.WriteHost(host)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func do(req *gemini.Request, via []*gemini.Request) (*gemini.Response, *gemini.Request, error) {
|
||||||
|
client := gemini.Client{
|
||||||
|
TrustCertificate: trustCertificate,
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
resp, err := client.Do(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return resp, req, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Status.Class() == gemini.StatusRedirect {
|
||||||
|
via = append(via, req)
|
||||||
|
if len(via) > 5 {
|
||||||
|
return resp, req, errors.New("too many redirects")
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := url.Parse(resp.Meta)
|
||||||
|
if err != nil {
|
||||||
|
return resp, req, err
|
||||||
|
}
|
||||||
|
if target.Scheme != "gemini" && target.Scheme != "" {
|
||||||
|
return resp, req, errors.New(fmt.Sprintf("tried to redirect to scheme %s", target.Scheme))
|
||||||
|
}
|
||||||
|
target = req.URL.ResolveReference(target)
|
||||||
|
redirect := *req
|
||||||
|
redirect.URL = target
|
||||||
|
return do(&redirect, via)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, req, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: konbata <url>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: %s <url>\n", os.Args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := gemini.NewRequest(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, req, err := do(req, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.Status.Class() != gemini.StatusSuccess {
|
||||||
|
log.Fatalf("%d %s", resp.Status, resp.Meta)
|
||||||
|
}
|
||||||
|
aw := AtomWriter{
|
||||||
|
Title: "",
|
||||||
|
Items: nil,
|
||||||
|
}
|
||||||
|
gemini.ParseLines(resp.Body, aw.Handle)
|
||||||
|
sort.Sort(ByTime(aw.Items))
|
||||||
|
feed := &Feed{
|
||||||
|
XMLNS: "http://www.w3.org/2005/Atom",
|
||||||
|
Link: &FeedLink{
|
||||||
|
Href: req.URL.String(),
|
||||||
|
},
|
||||||
|
Id: req.URL.String(),
|
||||||
|
Entries: nil,
|
||||||
|
}
|
||||||
|
if aw.Title == "" {
|
||||||
|
feed.Title = req.URL.String()
|
||||||
|
} else {
|
||||||
|
feed.Title = aw.Title
|
||||||
|
}
|
||||||
|
if len(aw.Items) == 0 {
|
||||||
|
feed.Updated = time.Now().Format(time.RFC3339)
|
||||||
|
} else {
|
||||||
|
feed.Updated = aw.Items[0].Date.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
for i := len(aw.Items); i != 0; i-- {
|
||||||
|
item := aw.Items[i - 1]
|
||||||
|
link, err := req.URL.Parse(item.Link)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
feed.Entries = append(feed.Entries, &FeedEntry{
|
||||||
|
Title: item.Title,
|
||||||
|
Link: &EntryLink{
|
||||||
|
Href: link.String(),
|
||||||
|
Rel: "alternate",
|
||||||
|
},
|
||||||
|
Id: link.String(),
|
||||||
|
Updated: item.Date.Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
out, _ := xml.MarshalIndent(feed, "", " ")
|
||||||
|
fmt.Println(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feed struct {
|
||||||
|
XMLName xml.Name `xml:"feed"`
|
||||||
|
XMLNS string `xml:"xmlns,attr"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link *FeedLink `xml:"link"`
|
||||||
|
Updated string `xml:"updated"`
|
||||||
|
Id string `xml:"id"`
|
||||||
|
Entries []*FeedEntry `xml:"entry"`
|
||||||
|
}
|
||||||
|
type FeedLink struct {
|
||||||
|
XMLName xml.Name `xml:"link"`
|
||||||
|
Href string `xml:"href,attr"`
|
||||||
|
}
|
||||||
|
type FeedEntry struct {
|
||||||
|
XMLName xml.Name `xml:"entry"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link *EntryLink `xml:"link"`
|
||||||
|
Id string `xml:"id"`
|
||||||
|
Updated string `xml:"updated"`
|
||||||
|
}
|
||||||
|
type EntryLink struct {
|
||||||
|
XMLName xml.Name `xml:"link"`
|
||||||
|
Href string `xml:"href,attr"`
|
||||||
|
Rel string `xml:"rel,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedItem struct {
|
||||||
|
Date time.Time
|
||||||
|
Title string
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByTime []FeedItem
|
||||||
|
func (a ByTime) Len() int {
|
||||||
|
return len(a)
|
||||||
|
}
|
||||||
|
func (a ByTime) Less(i, j int) bool {
|
||||||
|
return a[i].Date.Before(a[j].Date)
|
||||||
|
}
|
||||||
|
func (a ByTime) Swap(i, j int) {
|
||||||
|
a[i], a[j] = a[j], a[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomWriter struct {
|
||||||
|
Title string
|
||||||
|
Items []FeedItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AtomWriter) Handle(line gemini.Line) {
|
||||||
|
switch line := line.(type) {
|
||||||
|
case gemini.LineLink:
|
||||||
|
runes := []rune(line.Name)
|
||||||
|
t, err := time.Parse("2006-01-02", string(runes[:10]))
|
||||||
|
if err == nil {
|
||||||
|
a.Items = append(a.Items, FeedItem{
|
||||||
|
Date: t,
|
||||||
|
Title: strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(string(runes[10:])), ":-")),
|
||||||
|
Link: string(line.URL),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case gemini.LineHeading1:
|
||||||
|
if a.Title == "" {
|
||||||
|
a.Title = string(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue