x-1/files.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)
}