package updater import ( "context" "fmt" "sort" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/common" ) func (u *Updater) FetchServers(ctx context.Context, minServers int) ( servers []models.Server, err error, ) { switch { case u.username == "": return nil, fmt.Errorf("%w: username is empty", common.ErrCredentialsMissing) case u.password == "": return nil, fmt.Errorf("%w: password is empty", common.ErrCredentialsMissing) } apiClient, err := newAPIClient(ctx, u.client) if err != nil { return nil, fmt.Errorf("creating API client: %w", err) } cookie, err := apiClient.authenticate(ctx, u.username, u.password) if err != nil { return nil, fmt.Errorf("authentifying with Proton: %w", err) } data, err := apiClient.fetchServers(ctx, cookie) if err != nil { return nil, fmt.Errorf("fetching logical servers: %w", err) } countryCodes := constants.CountryCodes() var count int for _, logicalServer := range data.LogicalServers { count += len(logicalServer.Servers) } if count < minServers { return nil, fmt.Errorf("%w: %d and expected at least %d", common.ErrNotEnoughServers, count, minServers) } ipToServer := make(ipToServers, count) for _, logicalServer := range data.LogicalServers { region := getStringValue(logicalServer.Region) city := getStringValue(logicalServer.City) // TODO v4 remove `name` field because of // https://github.com/qdm12/gluetun/issues/1018#issuecomment-1151750179 name := logicalServer.Name //nolint:lll // See https://github.com/ProtonVPN/protonvpn-nm-lib/blob/31d5f99fbc89274e4e977a11e7432c0eab5a3ef8/protonvpn_nm_lib/enums.py#L44-L49 featuresBits := logicalServer.Features features := features{ secureCore: featuresBits&1 != 0, tor: featuresBits&2 != 0, p2p: featuresBits&4 != 0, stream: featuresBits&8 != 0, // ipv6: featuresBits&16 != 0, - unused. } //nolint:lll // See https://github.com/ProtonVPN/protonvpn-nm-lib/blob/31d5f99fbc89274e4e977a11e7432c0eab5a3ef8/protonvpn_nm_lib/enums.py#L56-L62 free := false if logicalServer.Tier == nil { u.warner.Warn("tier field not set for server " + logicalServer.Name) } else if *logicalServer.Tier == 0 { free = true } for _, physicalServer := range logicalServer.Servers { if physicalServer.Status == 0 { // disabled so skip server u.warner.Warn("ignoring server " + physicalServer.Domain + " with status 0") continue } hostname := physicalServer.Domain entryIP := physicalServer.EntryIP wgPubKey := physicalServer.X25519PublicKey // Note: for multi-hop use the server name or hostname // instead of the country countryCode := logicalServer.ExitCountry country, warning := codeToCountry(countryCode, countryCodes) if warning != "" { u.warner.Warn(warning) } ipToServer.add(country, region, city, name, hostname, wgPubKey, free, entryIP, features) } } if ipToServer.numberOfServers() < minServers { return nil, fmt.Errorf("%w: %d and expected at least %d", common.ErrNotEnoughServers, len(ipToServer), minServers) } servers = ipToServer.toServersSlice() sort.Sort(models.SortableServers(servers)) return servers, nil } func getStringValue(ptr *string) string { if ptr == nil { return "" } return *ptr }