mirror of
https://github.com/qdm12/gluetun.git
synced 2026-05-06 20:10:11 +02:00
25f67cd170
- new directory structure containing manifest.json and one json file per provider, by default. - the manifest.json file can specify a filepath for each vpn provider - each vpn provider json data file can contain the `"preferred": true` field to enforce it is used even if outdated, unless there is a version mismatch - `STORAGE_SERVERS_DIRECTORY_PATH` replaces `STORAGE_FILEPATH` (which is now a migration source only). It sets the directory where server manifest and per-provider JSON files are stored (default: `/gluetun/servers/`). - First-run migration: On startup, gluetun checks for the old /gluetun/servers.json file; if found and no new manifest exists, it automatically migrates all data to /gluetun/servers/ directory structure - Silent fallback: If legacy file isn't found, uses the new directory path normally - Legacy cleanup: After successful migration, attempts to remove the old fat JSON file (logs warning only if removal fails, e.g., read-only bind mounts) Co-authored-by: Copilot <copilot@github.com>
135 lines
4.5 KiB
Go
135 lines
4.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/qdm12/dns/v2/pkg/doh"
|
|
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
|
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
|
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
|
"github.com/qdm12/gluetun/internal/provider"
|
|
"github.com/qdm12/gluetun/internal/publicip/api"
|
|
"github.com/qdm12/gluetun/internal/updater"
|
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
|
"github.com/qdm12/gluetun/internal/updater/unzip"
|
|
)
|
|
|
|
var (
|
|
ErrNoProviderSpecified = errors.New("no provider was specified")
|
|
ErrUsernameMissing = errors.New("username is required for this provider")
|
|
ErrPasswordMissing = errors.New("password is required for this provider")
|
|
)
|
|
|
|
type UpdaterLogger interface {
|
|
Info(s string)
|
|
Infof(format string, args ...any)
|
|
Warn(s string)
|
|
Warnf(format string, args ...any)
|
|
Error(s string)
|
|
}
|
|
|
|
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
|
options := settings.Updater{}
|
|
var endUserMode, updateAll bool
|
|
var dnsServer, csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
|
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
|
flagSet.BoolVar(&endUserMode, "enduser", false, // TODO v4: remove
|
|
"Write results to /gluetun/servers/ (for end users)")
|
|
flagSet.StringVar(&dnsServer, "dns", "", "no longer used, your DNS will use DoH with Cloudflare and Google")
|
|
const defaultMinRatio = 0.8
|
|
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
|
|
"Minimum ratio of servers to find for the update to succeed")
|
|
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
|
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
|
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
|
flagSet.StringVar(&protonUsername, "proton-username", "",
|
|
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
|
|
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
|
|
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
|
if err := flagSet.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
if dnsServer != "" {
|
|
logger.Warn("The -dns flag is no longer used, your DNS will use DoH with Cloudflare and Google")
|
|
}
|
|
|
|
if updateAll {
|
|
options.Providers = providers.All()
|
|
} else {
|
|
if csvProviders == "" {
|
|
return fmt.Errorf("%w", ErrNoProviderSpecified)
|
|
}
|
|
options.Providers = strings.Split(csvProviders, ",")
|
|
}
|
|
|
|
if slices.Contains(options.Providers, providers.Protonvpn) {
|
|
if protonEmail == "" && protonUsername != "" {
|
|
protonEmail = protonUsername + "@protonmail.com"
|
|
logger.Warn("use -proton-email instead of -proton-username in the future. " +
|
|
"This assumes the email is " + protonEmail + " and may not work.")
|
|
}
|
|
options.ProtonEmail = &protonEmail
|
|
options.ProtonPassword = &protonPassword
|
|
}
|
|
|
|
options.SetDefaults(options.Providers[0])
|
|
|
|
err := options.Validate()
|
|
if err != nil {
|
|
return fmt.Errorf("options validation failed: %w", err)
|
|
}
|
|
|
|
storage, err := setupStorage(logger)
|
|
if err != nil {
|
|
return fmt.Errorf("creating servers storage: %w", err)
|
|
}
|
|
|
|
dohSettings := doh.Settings{
|
|
UpstreamResolvers: []dnsprovider.Provider{
|
|
dnsprovider.Cloudflare(),
|
|
dnsprovider.Google(),
|
|
},
|
|
}
|
|
dnsDialer, err := doh.New(dohSettings)
|
|
if err != nil {
|
|
return fmt.Errorf("creating DoH dialer: %w", err)
|
|
}
|
|
|
|
const clientTimeout = 10 * time.Second
|
|
httpClient := &http.Client{Timeout: clientTimeout}
|
|
unzipper := unzip.New(httpClient)
|
|
parallelResolver := resolver.NewParallelResolver(dnsDialer)
|
|
nameTokenPairs := []api.NameToken{
|
|
{Name: string(api.IPInfo), Token: ipToken},
|
|
{Name: string(api.IP2Location)},
|
|
{Name: string(api.IfConfigCo)},
|
|
}
|
|
fetchers, err := api.New(nameTokenPairs, httpClient)
|
|
if err != nil {
|
|
return fmt.Errorf("creating public IP fetchers: %w", err)
|
|
}
|
|
ipFetcher := api.NewResilient(fetchers, logger)
|
|
|
|
openvpnFileExtractor := extract.New()
|
|
|
|
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
|
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
|
|
|
updater := updater.New(httpClient, storage, providers, logger)
|
|
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
|
if err != nil {
|
|
return fmt.Errorf("updating server information: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|