426 lines
8.2 KiB
Go
426 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
)
|
|
|
|
type ConfigMain struct {
|
|
DefaultScheme string `toml:"default_scheme"`
|
|
SoftWrap int `toml:"soft_wrap"`
|
|
DownloadFolder string `toml:"download_folder"`
|
|
VimKeys bool `toml:"vim_keys"`
|
|
Quiet bool `toml:"quiet"`
|
|
Pager string `toml:"pager"`
|
|
Timeout duration `toml:"duration"`
|
|
SavedHistoryDepth int `toml:"saved_history_depth"`
|
|
}
|
|
|
|
type Config struct {
|
|
ConfigMain `toml:"main"`
|
|
|
|
Handlers map[string]string `toml:"handlers"`
|
|
}
|
|
|
|
type duration struct{ time.Duration }
|
|
|
|
func (d *duration) UnmarshalText(text []byte) error {
|
|
var err error
|
|
d.Duration, err = time.ParseDuration(string(text))
|
|
return err
|
|
}
|
|
|
|
func getConfig() (*Config, error) {
|
|
home := os.Getenv("HOME")
|
|
path := os.Getenv("XDG_CONFIG_HOME")
|
|
if path == "" {
|
|
path = filepath.Join(home, ".config")
|
|
}
|
|
path = filepath.Join(path, "x-1", "config.toml")
|
|
|
|
if err := ensurePath(path); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := Config{
|
|
ConfigMain: ConfigMain{
|
|
VimKeys: true,
|
|
DefaultScheme: "gemini",
|
|
SoftWrap: 100,
|
|
DownloadFolder: home,
|
|
Quiet: false,
|
|
Pager: "auto",
|
|
Timeout: duration{
|
|
time.Duration(10 * time.Second),
|
|
},
|
|
SavedHistoryDepth: 30,
|
|
},
|
|
Handlers: map[string]string{},
|
|
}
|
|
if _, err := toml.DecodeFile(path, &c); err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.HasPrefix(c.DownloadFolder, "~") {
|
|
c.DownloadFolder = home + c.DownloadFolder[1:]
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func getMarks() (map[string]string, error) {
|
|
path, err := marksFilePath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
marks := make(map[string]string)
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
rdr := bufio.NewScanner(f)
|
|
for rdr.Scan() {
|
|
line := rdr.Text()
|
|
name, target, _ := strings.Cut(line, ":")
|
|
marks[name] = target
|
|
}
|
|
if err := rdr.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return marks, nil
|
|
}
|
|
|
|
func saveMarks(marks map[string]string) error {
|
|
path, err := marksFilePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
for name, target := range marks {
|
|
_, err := fmt.Fprintf(f, "%s:%s\n", name, target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func marksFilePath() (string, error) {
|
|
return dataFilePath("marks")
|
|
}
|
|
|
|
func getTours() (map[string]*Tour, error) {
|
|
path, err := toursFilePath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tours := make(map[string]*Tour)
|
|
var current *Tour
|
|
var currentName string
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
rdr := bufio.NewScanner(f)
|
|
for rdr.Scan() {
|
|
line := rdr.Text()
|
|
if strings.HasSuffix(line, ":") {
|
|
if currentName != "" {
|
|
tours[currentName] = current
|
|
}
|
|
currentName = strings.TrimSuffix(line, ":")
|
|
current = &Tour{}
|
|
} else {
|
|
u, err := url.Parse(line)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
current.Links = append(current.Links, u)
|
|
}
|
|
}
|
|
if err := rdr.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if currentName != "" {
|
|
tours[currentName] = current
|
|
}
|
|
|
|
return tours, nil
|
|
}
|
|
|
|
func saveTours(tours map[string]*Tour) error {
|
|
path, err := toursFilePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
for name, tour := range tours {
|
|
if len(tour.Links) == 0 {
|
|
continue
|
|
}
|
|
|
|
if _, err := fmt.Fprintf(f, "%s:\n", name); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, link := range tour.Links {
|
|
if _, err := fmt.Fprintf(f, "%s\n", link.String()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func toursFilePath() (string, error) {
|
|
return dataFilePath("tours")
|
|
}
|
|
|
|
func getTofuStore() error {
|
|
tofuFilePath, err := dataFilePath("tofu")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tofuStore = map[string]string{}
|
|
|
|
f, err := os.Open(tofuFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
rdr := bufio.NewScanner(f)
|
|
for rdr.Scan() {
|
|
domain, certhash, _ := strings.Cut(rdr.Text(), ":")
|
|
tofuStore[domain] = certhash
|
|
}
|
|
if err := rdr.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func saveTofuStore(store map[string]string) error {
|
|
tofuFilePath, err := dataFilePath("tofu")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.OpenFile(tofuFilePath, os.O_WRONLY|os.O_TRUNC, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
for domain, certhash := range store {
|
|
if _, err := fmt.Fprintf(f, "%s:%s\n", domain, certhash); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func dataFilePath(filename string) (string, error) {
|
|
home := os.Getenv("HOME")
|
|
path := os.Getenv("XDG_DATA_HOME")
|
|
if path == "" {
|
|
path = filepath.Join(home, ".local", "share")
|
|
}
|
|
path = filepath.Join(path, "x-1", filename)
|
|
|
|
if err := ensurePath(path); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return path, nil
|
|
}
|
|
|
|
func ensurePath(fpath string) error {
|
|
if _, err := os.Stat(fpath); errors.Is(err, syscall.ENOENT) {
|
|
if err := os.MkdirAll(filepath.Dir(fpath), 0o700); err != nil {
|
|
return err
|
|
}
|
|
f, err := os.OpenFile(fpath, os.O_RDWR|os.O_CREATE, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = f.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getIdentities() (Identities, error) {
|
|
idents := Identities{
|
|
ByName: map[string]*tls.Config{},
|
|
ByDomain: map[string]*tls.Config{},
|
|
ByFolder: map[string]*tls.Config{},
|
|
ByPage: map[string]*tls.Config{},
|
|
}
|
|
|
|
manifest, err := dataFilePath("identities")
|
|
if err != nil {
|
|
return idents, err
|
|
}
|
|
|
|
f, err := os.Open(manifest)
|
|
if err != nil {
|
|
return idents, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
var curident *tls.Config
|
|
rdr := bufio.NewScanner(f)
|
|
for rdr.Scan() {
|
|
line := rdr.Text()
|
|
if strings.HasPrefix(line, ":") {
|
|
kind, location, _ := strings.Cut(line[1:], " ")
|
|
switch kind {
|
|
case "domain":
|
|
idents.ByDomain[location] = curident
|
|
case "folder":
|
|
idents.ByFolder[location] = curident
|
|
case "page":
|
|
idents.ByPage[location] = curident
|
|
}
|
|
} else {
|
|
name := strings.TrimSuffix(line, ":")
|
|
curident, err = getIdentity(name)
|
|
if err != nil {
|
|
return idents, err
|
|
}
|
|
idents.ByName[name] = curident
|
|
}
|
|
}
|
|
if err := rdr.Err(); err != nil {
|
|
return idents, err
|
|
}
|
|
|
|
return idents, nil
|
|
}
|
|
|
|
func saveIdentities(idents Identities) error {
|
|
manifest, err := dataFilePath("identities")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.OpenFile(manifest, os.O_WRONLY|os.O_TRUNC, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
for name, ident := range idents.ByName {
|
|
if _, err := fmt.Fprintf(f, "%s:\n", name); err != nil {
|
|
return err
|
|
}
|
|
|
|
for domain, id := range idents.ByDomain {
|
|
if id != ident {
|
|
continue
|
|
}
|
|
if _, err := fmt.Fprintf(f, ":domain %s\n", domain); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for folder, id := range idents.ByFolder {
|
|
if id != ident {
|
|
continue
|
|
}
|
|
if _, err := fmt.Fprintf(f, ":folder %s\n", folder); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for page, id := range idents.ByPage {
|
|
if id != ident {
|
|
continue
|
|
}
|
|
if _, err := fmt.Fprintf(f, ":page %s\n", page); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getIdentity(name string) (*tls.Config, error) {
|
|
fpath, err := dataFilePath("ident/" + name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cert, err := tls.LoadX509KeyPair(fpath, fpath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return identityForCert(cert), nil
|
|
}
|
|
|
|
func saveIdentity(name string, privkeyDER, certDER []byte) (string, error) {
|
|
fpath, err := dataFilePath("ident/" + name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_TRUNC, 0o600)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
if err := pem.Encode(f, &pem.Block{Type: "PRIVATE KEY", Bytes: privkeyDER}); err != nil {
|
|
return "", err
|
|
}
|
|
if err := pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return fpath, nil
|
|
}
|
|
|
|
func removeIdentity(name string) error {
|
|
fpath, err := dataFilePath("ident/" + name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.Remove(fpath)
|
|
}
|