arblant
firefox-vpn-client / core / internal / service / paths.go
paths.go159 lines · 4.7 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
package service

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/armanlife/firefox-vpn-client/core/internal/accounts"
	"github.com/armanlife/firefox-vpn-client/core/internal/config"
	"github.com/armanlife/firefox-vpn-client/core/internal/logx"
)

// Files the service keeps in its data folder.
const (
	statusFileName    = "status.json"
	selectionFileName = "selection.json"
	stateFileName     = "state.json"
	localTokenName    = "local-api.token"
	logsDirName       = "logs"
	routingDirName    = "routing"
	sessionKeyName    = "dashboard.key"
	serverListName    = "servers.json"
)

type DataPaths struct {
	Dir        string
	Config     string
	Status     string
	Selection  string
	State      string
	LocalToken string
	// LogDir holds the daily log files (see logx.DayFile).
	LogDir string
	// RoutingDir holds the routing lists (geosite.dat, geoip.dat).
	RoutingDir string
	// SessionKey signs the dashboard's session cookies.
	SessionKey string
	// ServerList keeps the last server list fetched from Mozilla, for
	// when the next fetch fails and to show the locations at once.
	ServerList string
}

// ResolveDataPaths returns the data folder (next to the executable unless
// overridden) and the files inside it, and checks that it is writable.
func ResolveDataPaths(override string) (DataPaths, error) {
	dir := strings.TrimSpace(override)
	if dir == "" {
		var err error
		if dir, err = config.DefaultDataDir(); err != nil {
			return DataPaths{}, err
		}
	}
	dir, err := filepath.Abs(dir)
	if err != nil {
		return DataPaths{}, err
	}
	if err := config.CheckWritable(dir); err != nil {
		return DataPaths{}, err
	}
	return DataPaths{
		Dir:        dir,
		Config:     filepath.Join(dir, config.FileName),
		Status:     filepath.Join(dir, statusFileName),
		Selection:  filepath.Join(dir, selectionFileName),
		State:      filepath.Join(dir, stateFileName),
		LocalToken: filepath.Join(dir, localTokenName),
		LogDir:     filepath.Join(dir, logsDirName),
		RoutingDir: filepath.Join(dir, routingDirName),
		SessionKey: filepath.Join(dir, sessionKeyName),
		ServerList: filepath.Join(dir, serverListName),
	}, nil
}

// LoadConfig loads (or creates) the config and applies its process
// wide settings: log detail, DNS, and the API proxy.
func LoadConfig(paths DataPaths, forceVerbose bool) (config.Config, error) {
	config.Warn = func(message string) { logx.Warn("%s", message) }
	accounts.Warn = func(message string) { logx.Warn("%s", message) }
	cfg, created, err := config.Load(paths.Config)
	if err != nil {
		return config.Config{}, err
	}
	if created {
		logx.Info("created default config path=%s", paths.Config)
	}
	logx.Verbose.Store(cfg.Logging.Verbose || forceVerbose)
	if err := configureDNS(cfg.DNS); err != nil {
		return config.Config{}, fmt.Errorf("invalid dns.servers: %w", err)
	}
	if err := configureAPIRelay(cfg.Network.APIRelay); err != nil {
		return config.Config{}, fmt.Errorf("invalid network.api_relay: %w", err)
	}
	return cfg, nil
}

// OpenAccounts opens the account store and reconciles the configured
// priority order with the stored accounts. It returns the enabled account
// ids in order.
func OpenAccounts(paths DataPaths, cfg *config.Config) (*accounts.Store, []string, error) {
	store, err := accounts.OpenStore(paths.Dir)
	if err != nil {
		return nil, nil, err
	}
	list, err := store.List()
	if err != nil {
		return nil, nil, err
	}
	enabled, changed := ReconcileAccountOrder(cfg, list)
	if changed {
		if err := config.Save(paths.Config, *cfg); err != nil {
			return nil, nil, fmt.Errorf("saving account order: %w", err)
		}
	}
	return store, enabled, nil
}

// ReconcileAccountOrder keeps configured entries whose account exists,
// appends stored accounts missing from the order, and returns the enabled
// ids in priority order.
func ReconcileAccountOrder(cfg *config.Config, list []accounts.Account) (enabled []string, changed bool) {
	stored := make(map[string]bool, len(list))
	for _, acc := range list {
		stored[acc.ID] = true
	}
	order := make([]config.AccountRef, 0, len(list))
	listed := make(map[string]bool, len(cfg.Accounts.Order))
	for _, ref := range cfg.Accounts.Order {
		if !stored[ref.ID] {
			logx.Warn("config lists an unknown account; removing it from the order id=%s", ref.ID)
			changed = true
			continue
		}
		listed[ref.ID] = true
		order = append(order, ref)
	}
	for _, acc := range list {
		if !listed[acc.ID] {
			order = append(order, config.AccountRef{ID: acc.ID, Enabled: true})
			changed = true
		}
	}
	cfg.Accounts.Order = order
	for _, ref := range order {
		if ref.Enabled {
			enabled = append(enabled, ref.ID)
		}
	}
	return enabled, changed
}

// removeFile deletes path, ignoring a missing file.
func removeFile(path string) error {
	if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	return nil
}