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