Compare commits

..

4 Commits

Author SHA1 Message Date
blank X 280703edd8
Store permanent errors 2021-08-10 16:07:09 +07:00
blank X 34fffa9d37
Store permanent redirects 2021-08-10 15:26:26 +07:00
blank X a94dd6fb44
Reuse client and ctx 2021-08-10 14:46:01 +07:00
blank X 3718689e02
Check for non-success in do 2021-08-10 13:09:51 +07:00
1 changed files with 204 additions and 9 deletions

213
main.go
View File

@ -6,8 +6,10 @@ import (
"log"
"sort"
"time"
"bufio"
"errors"
"context"
"strconv"
"strings"
"net/url"
"crypto/x509"
@ -21,6 +23,8 @@ import (
var (
hosts tofu.KnownHosts
hostsfile *tofu.HostWriter
predirs map[string]*url.URL
perrors map[string]PError
)
func xdgDataHome() string {
@ -41,6 +45,154 @@ func init() {
if err != nil {
log.Fatal(err)
}
err = populatePRedirs()
if err != nil {
log.Fatal(err)
}
err = populatePErrors()
if err != nil {
log.Fatal(err)
}
}
func populatePRedirs() error {
file, err := os.OpenFile(filepath.Join(xdgDataHome(), "konbata", "predirs"), os.O_RDONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
predirs = make(map[string]*url.URL)
for scanner.Scan() {
urls := strings.Split(scanner.Text(), " ")
if len(urls) != 2 {
continue
}
url0, err := url.Parse(urls[0])
if err != nil {
continue
}
if url0.Scheme != "gemini" {
continue
}
url1, err := url.Parse(urls[1])
if err != nil {
continue
}
if url1.Scheme != "gemini" {
continue
}
predirs[url0.String()] = url1
}
return nil
}
func savePRedirs() error {
file, err := os.OpenFile(filepath.Join(xdgDataHome(), "konbata", "predirs"), os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer file.Close()
err = file.Truncate(0)
if err != nil {
return err
}
writer := bufio.NewWriter(file)
for url0, url1 := range predirs {
_, err = writer.WriteString(url0)
if err != nil {
return err
}
_, err = writer.WriteRune(' ')
if err != nil {
return err
}
_, err = writer.WriteString(url1.String())
if err != nil {
return err
}
_, err = writer.WriteRune('\n')
if err != nil {
return err
}
}
return writer.Flush()
}
func populatePErrors() error {
file, err := os.OpenFile(filepath.Join(xdgDataHome(), "konbata", "perrors"), os.O_RDONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
perrors = make(map[string]PError)
unescaper := strings.NewReplacer("\\n", "\n", "\\\\", "\\")
for scanner.Scan() {
values := strings.SplitN(scanner.Text(), " ", 3)
if len(values) != 3 {
continue
}
url, err := url.Parse(values[0])
if err != nil {
continue
}
if url.Scheme != "gemini" {
continue
}
code, err := strconv.Atoi(values[1])
if err != nil {
continue
}
perrors[url.String()] = PError{
code: gemini.Status(code),
message: unescaper.Replace(values[2]),
}
}
return nil
}
func savePErrors() error {
file, err := os.OpenFile(filepath.Join(xdgDataHome(), "konbata", "perrors"), os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer file.Close()
err = file.Truncate(0)
if err != nil {
return err
}
writer := bufio.NewWriter(file)
escaper := strings.NewReplacer("\\", "\\\\", "\n", "\\n")
for url, perror := range perrors {
_, err = writer.WriteString(url)
if err != nil {
return err
}
_, err = writer.WriteRune(' ')
if err != nil {
return err
}
_, err = writer.WriteString(strconv.Itoa(int(perror.code)))
if err != nil {
return err
}
_, err = writer.WriteRune(' ')
if err != nil {
return err
}
_, err = escaper.WriteString(writer, perror.message)
if err != nil {
return err
}
_, err = writer.WriteRune('\n')
if err != nil {
return err
}
}
return writer.Flush()
}
func trustCertificate(hostname string, cert *x509.Certificate) error {
@ -59,16 +211,50 @@ func trustCertificate(hostname string, cert *x509.Certificate) error {
return nil
}
func do(req *gemini.Request, via []*gemini.Request) (*gemini.Response, *gemini.Request, error) {
client := gemini.Client{
TrustCertificate: trustCertificate,
func do(client gemini.Client, ctx context.Context, req *gemini.Request, via []*gemini.Request) (*gemini.Response, *gemini.Request, error) {
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)
}
if perror, exists := perrors[req.URL.String()]; exists {
return nil, req, errors.New(fmt.Sprintf("%d %s", perror.code, perror.message))
}
ctx := context.Background()
resp, err := client.Do(ctx, req)
if err != nil {
return resp, req, err
}
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
}
}
if resp.Status.Class() == gemini.StatusPermanentFailure {
perrors[req.URL.String()] = PError{
code: resp.Status,
message: resp.Meta,
}
err = savePErrors()
if err != nil {
return resp, req, err
}
}
if resp.Status.Class() == gemini.StatusRedirect {
via = append(via, req)
if len(via) > 5 {
@ -85,7 +271,10 @@ func do(req *gemini.Request, via []*gemini.Request) (*gemini.Response, *gemini.R
target = req.URL.ResolveReference(target)
redirect := *req
redirect.URL = target
return do(&redirect, via)
return do(client, ctx, &redirect, via)
}
if resp.Status.Class() != gemini.StatusSuccess {
return resp, req, errors.New(fmt.Sprintf("%d %s", resp.Status, resp.Meta))
}
return resp, req, err
@ -106,15 +295,16 @@ func main() {
log.Fatal(err)
}
resp, req, err := do(req, nil)
client := gemini.Client{
TrustCertificate: trustCertificate,
}
ctx := context.Background()
resp, req, err := do(client, ctx, 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)
}
if resp.Meta != "text/gemini" && !strings.HasPrefix(resp.Meta, "text/gemini;") {
log.Fatal("mime type is not text/gemini")
}
@ -208,6 +398,11 @@ func (a ByTime) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
type PError struct {
code gemini.Status
message string
}
type AtomWriter struct {
Title string
Items []FeedItem