konbata/main.go

203 lines
4.6 KiB
Go
Raw Normal View History

2021-08-09 14:14:56 +00:00
package main
import (
"context"
"encoding/xml"
2021-08-11 13:22:30 +00:00
"errors"
"fmt"
2022-07-19 13:48:50 +00:00
"io"
2021-08-11 13:22:30 +00:00
"log"
"net/url"
"os"
"sort"
"strings"
"time"
2021-08-09 14:14:56 +00:00
"git.sr.ht/~adnano/go-gemini"
)
2021-08-28 23:43:12 +00:00
const TIMEOUT_NS time.Duration = time.Duration(60_000_000_000)
2022-09-01 18:32:54 +00:00
const MAX_FILE_SIZE int64 = 512 * 1024
2022-07-19 13:48:50 +00:00
2021-08-09 14:14:56 +00:00
func init() {
2023-01-01 17:11:02 +00:00
err := populateHosts()
2021-08-09 14:14:56 +00:00
if err != nil {
log.Fatal(err)
}
2021-08-10 08:26:26 +00:00
err = populatePRedirs()
if err != nil {
log.Fatal(err)
}
2021-08-10 09:07:09 +00:00
err = populatePErrors()
if err != nil {
log.Fatal(err)
}
2021-08-10 08:26:26 +00:00
}
2021-08-10 07:46:01 +00:00
func do(client gemini.Client, ctx context.Context, req *gemini.Request, via []*gemini.Request) (*gemini.Response, *gemini.Request, error) {
2021-08-10 08:26:26 +00:00
if target, exists := predirs[req.URL.String()]; exists {
via = append(via, req)
if len(via) > 5 {
return nil, req, errors.New("too many redirects")
}
redirect := *req
redirect.URL = target
return do(client, ctx, &redirect, via)
}
2021-08-10 09:07:09 +00:00
if perror, exists := perrors[req.URL.String()]; exists {
return nil, req, errors.New(fmt.Sprintf("%d %s", perror.Code, perror.Message))
2021-08-10 09:07:09 +00:00
}
2021-08-09 14:14:56 +00:00
resp, err := client.Do(ctx, req)
if err != nil {
return resp, req, err
}
2021-08-10 08:26:26 +00:00
if resp.Status == gemini.StatusPermanentRedirect {
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)
predirs[req.URL.String()] = target
err = savePRedirs()
if err != nil {
return resp, req, err
}
}
2021-08-10 09:07:09 +00:00
if resp.Status.Class() == gemini.StatusPermanentFailure {
perrors[req.URL.String()] = PError{
2021-08-11 13:22:30 +00:00
Code: resp.Status,
Message: resp.Meta,
2021-08-10 09:07:09 +00:00
}
err = savePErrors()
if err != nil {
return resp, req, err
}
}
2021-08-09 14:14:56 +00:00
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
2021-08-10 07:46:01 +00:00
return do(client, ctx, &redirect, via)
2021-08-09 14:14:56 +00:00
}
2021-08-10 06:09:51 +00:00
if resp.Status.Class() != gemini.StatusSuccess {
return resp, req, errors.New(fmt.Sprintf("%d %s", resp.Status, resp.Meta))
}
2021-08-09 14:14:56 +00:00
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)
}
2021-08-10 07:46:01 +00:00
client := gemini.Client{
2023-01-01 17:11:02 +00:00
TrustCertificate: TrustCertificate,
2021-08-10 07:46:01 +00:00
}
2021-08-28 23:43:12 +00:00
ctx, cancel := context.WithTimeout(context.Background(), TIMEOUT_NS)
defer cancel()
2021-08-10 07:46:01 +00:00
resp, req, err := do(client, ctx, req, nil)
2021-08-09 14:14:56 +00:00
if err != nil {
log.Fatal(err)
}
2022-09-01 18:32:54 +00:00
limitedBody := io.LimitedReader{
R: resp.Body,
N: MAX_FILE_SIZE,
}
2021-08-09 14:14:56 +00:00
defer resp.Body.Close()
2022-07-19 13:48:50 +00:00
mime, _, _ := strings.Cut(resp.Meta, ";")
2022-07-23 17:42:17 +00:00
if mime == "application/rss+xml" || mime == "application/rss" ||
mime == "application/atom+xml" || mime == "application/atom" ||
2022-07-23 17:46:50 +00:00
mime == "application/xml" || mime == "text/xml" {
2022-09-01 18:32:54 +00:00
out, err := io.ReadAll(&limitedBody)
2021-08-09 14:14:56 +00:00
if err != nil {
log.Fatal(err)
}
2022-09-01 18:32:54 +00:00
if limitedBody.N <= 0 {
log.Fatalf("up to %d bytes read from the body", MAX_FILE_SIZE)
}
2022-07-19 13:48:50 +00:00
fmt.Print(string(out))
} else if mime == "text/gemini" {
aw := AtomWriter{
Title: "",
Items: nil,
}
2022-09-01 18:32:54 +00:00
gemini.ParseLines(&limitedBody, aw.Handle)
if limitedBody.N <= 0 {
log.Fatalf("up to %d bytes read from the body", MAX_FILE_SIZE)
}
2022-07-19 13:48:50 +00:00
sort.Sort(ByTime(aw.Items))
feed := Feed{
XMLNS: "http://www.w3.org/2005/Atom",
Link: FeedLink{
Href: req.URL.String(),
2021-08-09 14:14:56 +00:00
},
2022-07-19 13:48:50 +00:00
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[len(aw.Items)-1].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, err := xml.MarshalIndent(feed, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
} else {
log.Fatal("unsupported mime type: ", mime)
2021-08-10 05:29:23 +00:00
}
2021-08-09 14:14:56 +00:00
}