chore(updater): move updater packages to pkg/updaters/<name>

This commit is contained in:
Quentin McGaw
2026-04-23 03:47:57 +00:00
parent 5b01324d5f
commit 13503b0ae0
268 changed files with 1602 additions and 1026 deletions
+2 -2
View File
@@ -10,12 +10,12 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/pkg/publicip/models"
"github.com/qdm12/gluetun/pkg/updaters/resolver"
"github.com/qdm12/gosettings/reader"
)
+3 -3
View File
@@ -17,11 +17,11 @@ import (
"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/storage"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
"github.com/qdm12/gluetun/pkg/publicip/api"
"github.com/qdm12/gluetun/pkg/updaters/resolver"
"github.com/qdm12/gluetun/pkg/updaters/unzip"
)
type UpdaterLogger interface {
@@ -6,9 +6,9 @@ import (
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gluetun/pkg/updaters/constants"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
+5 -5
View File
@@ -7,7 +7,7 @@ import (
"strings"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/pkg/updaters/constants"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
@@ -31,9 +31,9 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
// Validate Name
var validNames []string
switch vpnType {
case vpn.AmneziaWg:
case constants.AmneziaWg:
validNames = []string{providers.Custom}
case vpn.OpenVPN:
case constants.OpenVPN:
validNames = providers.AllWithCustom()
validNames = append(validNames, "pia") // Retro-compatibility
// Remove Mullvad since it no longer supports OpenVPN as of January 15th, 2026
@@ -41,7 +41,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
validNames[mullvadIndex], validNames[len(validNames)-1] = validNames[len(validNames)-1], validNames[mullvadIndex]
validNames = validNames[:len(validNames)-1]
sort.Strings(validNames)
case vpn.Wireguard:
case constants.Wireguard:
validNames = []string{
providers.Airvpn,
providers.Custom,
@@ -122,7 +122,7 @@ func (p *Provider) read(r *reader.Reader, vpnType string) (err error) {
func readVPNServiceProvider(r *reader.Reader, vpnType string) (vpnProvider string) {
vpnProvider = r.String("VPN_SERVICE_PROVIDER", reader.RetroKeys("VPNSP"))
if vpnProvider == "" {
if vpnType != vpn.Wireguard && r.Get("OPENVPN_CUSTOM_CONFIG") != nil {
if vpnType != constants.Wireguard && r.Get("OPENVPN_CUSTOM_CONFIG") != nil {
// retro compatibility
return providers.Custom
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"path/filepath"
"github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gluetun/pkg/publicip/api"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
@@ -8,8 +8,8 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/configuration/settings/validation"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/pkg/updaters/constants"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
@@ -75,7 +75,7 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
filterChoicesGetter FilterChoicesGetter, warner Warner,
) (err error) {
switch ss.VPN {
case vpn.AmneziaWg, vpn.OpenVPN, vpn.Wireguard:
case constants.AmneziaWg, constants.OpenVPN, constants.Wireguard:
default:
return fmt.Errorf("VPN type is not valid: %s", ss.VPN)
}
@@ -108,7 +108,7 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
}
if ss.VPN == vpn.OpenVPN {
if ss.VPN == constants.OpenVPN {
err = ss.OpenVPN.validate(vpnServiceProvider)
if err != nil {
return fmt.Errorf("OpenVPN server selection settings: %w", err)
@@ -324,7 +324,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
}
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
ss.VPN = gosettings.DefaultComparable(ss.VPN, constants.OpenVPN)
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
@@ -414,7 +414,7 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node.Appendf("Port forwarding only servers: yes")
}
if ss.VPN == vpn.OpenVPN {
if ss.VPN == constants.OpenVPN {
node.AppendNode(ss.OpenVPN.toLinesNode())
} else {
node.AppendNode(ss.Wireguard.toLinesNode())
@@ -506,7 +506,7 @@ func (ss *ServerSelection) read(r *reader.Reader,
return err
}
amneziawg := ss.VPN == vpn.AmneziaWg
amneziawg := ss.VPN == constants.AmneziaWg
err = ss.Wireguard.read(r, amneziawg)
if err != nil {
return err
+2 -2
View File
@@ -5,9 +5,9 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gluetun/pkg/updaters/constants"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
@@ -175,7 +175,7 @@ func (s Settings) Warnings() (warnings []string) {
}
if helpers.IsOneOf(s.VPN.Provider.Name, providers.SlickVPN) &&
s.VPN.Type == vpn.OpenVPN {
s.VPN.Type == constants.OpenVPN {
warnings = append(warnings, "OpenVPN 2.5 and 2.6 use OpenSSL 3 "+
"which prohibits the usage of weak security in today's standards. "+
s.VPN.Provider.Name+" uses weak security which is out "+
@@ -3,7 +3,7 @@ package settings
import (
"strings"
"github.com/qdm12/gluetun/internal/provider/surfshark/servers"
"github.com/qdm12/gluetun/pkg/updaters/providers/surfshark/servers"
)
func surfsharkRetroRegion(selection ServerSelection) (
@@ -3,7 +3,7 @@ package validation
import (
"sort"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/pkg/updaters/models"
)
func sortedInsert(ss []string, s string) []string {
@@ -1,7 +1,7 @@
package validation
import (
"github.com/qdm12/gluetun/internal/provider/surfshark/servers"
"github.com/qdm12/gluetun/pkg/updaters/providers/surfshark/servers"
)
// TODO remove in v4.
+9 -9
View File
@@ -3,7 +3,7 @@ package settings
import (
"fmt"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/pkg/updaters/constants"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
@@ -35,7 +35,7 @@ type VPN struct {
// TODO v4 remove pointer for receiver (because of Surfshark).
func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) {
// Validate Type
validVPNTypes := []string{vpn.AmneziaWg, vpn.OpenVPN, vpn.Wireguard}
validVPNTypes := []string{constants.AmneziaWg, constants.OpenVPN, constants.Wireguard}
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil {
return fmt.Errorf("VPN type is not valid: %w", err)
}
@@ -46,17 +46,17 @@ func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bo
}
switch v.Type {
case vpn.AmneziaWg:
case constants.AmneziaWg:
err = v.AmneziaWg.validate(v.Provider.Name, ipv6Supported)
if err != nil {
return fmt.Errorf("AmneziaWG settings: %w", err)
}
case vpn.OpenVPN:
case constants.OpenVPN:
err := v.OpenVPN.validate(v.Provider.Name)
if err != nil {
return fmt.Errorf("OpenVPN settings: %w", err)
}
case vpn.Wireguard:
case constants.Wireguard:
const amneziawg = false
err := v.Wireguard.validate(v.Provider.Name, ipv6Supported, amneziawg)
if err != nil {
@@ -97,7 +97,7 @@ func (v *VPN) OverrideWith(other VPN) {
}
func (v *VPN) setDefaults() {
v.Type = gosettings.DefaultComparable(v.Type, vpn.OpenVPN)
v.Type = gosettings.DefaultComparable(v.Type, constants.OpenVPN)
v.Provider.setDefaults()
v.AmneziaWg.setDefaults(v.Provider.Name)
v.OpenVPN.setDefaults(v.Provider.Name)
@@ -117,11 +117,11 @@ func (v VPN) toLinesNode() (node *gotree.Node) {
node.AppendNode(v.Provider.toLinesNode())
switch v.Type {
case vpn.AmneziaWg:
case constants.AmneziaWg:
node.AppendNode(v.AmneziaWg.toLinesNode())
case vpn.OpenVPN:
case constants.OpenVPN:
node.AppendNode(v.OpenVPN.toLinesNode())
case vpn.Wireguard:
case constants.Wireguard:
node.AppendNode(v.Wireguard.toLinesNode())
}
node.AppendNode(v.PMTUD.toLinesNode())
-256
View File
@@ -1,256 +0,0 @@
package constants
func CountryCodes() map[string]string {
return map[string]string{
"af": "Afghanistan",
"ax": "Aland Islands",
"al": "Albania",
"dz": "Algeria",
"as": "American Samoa",
"ad": "Andorra",
"ao": "Angola",
"ai": "Anguilla",
"aq": "Antarctica",
"ag": "Antigua and Barbuda",
"ar": "Argentina",
"am": "Armenia",
"aw": "Aruba",
"au": "Australia",
"at": "Austria",
"az": "Azerbaijan",
"bs": "Bahamas",
"bh": "Bahrain",
"bd": "Bangladesh",
"bb": "Barbados",
"by": "Belarus",
"be": "Belgium",
"bz": "Belize",
"bj": "Benin",
"bm": "Bermuda",
"bt": "Bhutan",
"bo": "Bolivia",
"bq": "Bonaire",
"ba": "Bosnia and Herzegovina",
"bw": "Botswana",
"bv": "Bouvet Island",
"br": "Brazil",
"io": "British Indian Ocean Territory",
"vg": "British Virgin Islands",
"bn": "Brunei Darussalam",
"bg": "Bulgaria",
"bf": "Burkina Faso",
"bi": "Burundi",
"kh": "Cambodia",
"cm": "Cameroon",
"ca": "Canada",
"cv": "Cape Verde",
"ky": "Cayman Islands",
"cf": "Central African Republic",
"td": "Chad",
"cl": "Chile",
"cn": "China",
"cx": "Christmas Island",
"cc": "Cocos Islands",
"co": "Colombia",
"km": "Comoros",
"cg": "Congo",
"ck": "Cook Islands",
"cr": "Costa Rica",
"ci": "Cote d'Ivoire",
"hr": "Croatia",
"cu": "Cuba",
"cw": "Curacao",
"cy": "Cyprus",
"cz": "Czech Republic",
"cd": "Democratic Republic of the Congo",
"dk": "Denmark",
"dj": "Djibouti",
"dm": "Dominica",
"do": "Dominican Republic",
"ec": "Ecuador",
"eg": "Egypt",
"sv": "El Salvador",
"gq": "Equatorial Guinea",
"er": "Eritrea",
"ee": "Estonia",
"et": "Ethiopia",
"fk": "Falkland Islands",
"fo": "Faroe Islands",
"fj": "Fiji",
"fi": "Finland",
"fr": "France",
"gf": "French Guiana",
"pf": "French Polynesia",
"tf": "French Southern Territories",
"ga": "Gabon",
"gm": "Gambia",
"ge": "Georgia",
"de": "Germany",
"gh": "Ghana",
"gi": "Gibraltar",
"gr": "Greece",
"gl": "Greenland",
"gd": "Grenada",
"gp": "Guadeloupe",
"gu": "Guam",
"gt": "Guatemala",
"gg": "Guernsey",
"gw": "Guinea-Bissau",
"gn": "Guinea",
"gy": "Guyana",
"ht": "Haiti",
"hm": "Heard Island and McDonald Islands",
"hn": "Honduras",
"hk": "Hong Kong",
"hu": "Hungary",
"is": "Iceland",
"in": "India",
"id": "Indonesia",
"ir": "Iran",
"iq": "Iraq",
"ie": "Ireland",
"im": "Isle of Man",
"il": "Israel",
"it": "Italy",
"jm": "Jamaica",
"jp": "Japan",
"je": "Jersey",
"jo": "Jordan",
"kz": "Kazakhstan",
"ke": "Kenya",
"ki": "Kiribati",
"kr": "Korea",
"kw": "Kuwait",
"kg": "Kyrgyzstan",
"la": "Lao People's Democratic Republic",
"lv": "Latvia",
"lb": "Lebanon",
"ls": "Lesotho",
"lr": "Liberia",
"ly": "Libya",
"li": "Liechtenstein",
"lt": "Lithuania",
"lu": "Luxembourg",
"mo": "Macao",
"mk": "Macedonia",
"mg": "Madagascar",
"mw": "Malawi",
"my": "Malaysia",
"mys": "Kuala Lumpur",
"mv": "Maldives",
"ml": "Mali",
"mt": "Malta",
"mh": "Marshall Islands",
"mq": "Martinique",
"mr": "Mauritania",
"mu": "Mauritius",
"yt": "Mayotte",
"mx": "Mexico",
"fm": "Micronesia",
"md": "Moldova",
"mc": "Monaco",
"mn": "Mongolia",
"me": "Montenegro",
"ms": "Montserrat",
"ma": "Morocco",
"mz": "Mozambique",
"mm": "Myanmar",
"na": "Namibia",
"nr": "Nauru",
"np": "Nepal",
"nl": "Netherlands",
"nc": "New Caledonia",
"nz": "New Zealand",
"ni": "Nicaragua",
"ne": "Niger",
"ng": "Nigeria",
"nu": "Niue",
"nf": "Norfolk Island",
"mp": "Northern Mariana Islands",
"no": "Norway",
"om": "Oman",
"pk": "Pakistan",
"pw": "Palau",
"ps": "Palestine, State of",
"pa": "Panama",
"pg": "Papua New Guinea",
"py": "Paraguay",
"pe": "Peru",
"ph": "Philippines",
"pn": "Pitcairn",
"pl": "Poland",
"pt": "Portugal",
"pr": "Puerto Rico",
"qa": "Qatar",
"re": "Reunion",
"ro": "Romania",
"ru": "Russian Federation",
"rw": "Rwanda",
"bl": "Saint Barthelemy",
"sh": "Saint Helena",
"kn": "Saint Kitts and Nevis",
"lc": "Saint Lucia",
"mf": "Saint Martin",
"pm": "Saint Pierre and Miquelon",
"vc": "Saint Vincent and the Grenadines",
"ws": "Samoa",
"sm": "San Marino",
"st": "Sao Tome and Principe",
"sa": "Saudi Arabia",
"sn": "Senegal",
"rs": "Serbia",
"sc": "Seychelles",
"sl": "Sierra Leone",
"sg": "Singapore",
"sx": "Sint Maarten",
"sk": "Slovakia",
"si": "Slovenia",
"sb": "Solomon Islands",
"so": "Somalia",
"za": "South Africa",
"gs": "South Georgia and the South Sandwich Islands",
"ss": "South Sudan",
"es": "Spain",
"lk": "Sri Lanka",
"sd": "Sudan",
"sr": "Suriname",
"sj": "Svalbard and Jan Mayen",
"sz": "Swaziland",
"se": "Sweden",
"ch": "Switzerland",
"sy": "Syrian Arab Republic",
"tw": "Taiwan",
"tj": "Tajikistan",
"tz": "Tanzania",
"th": "Thailand",
"tl": "Timor-Leste",
"tg": "Togo",
"tk": "Tokelau",
"to": "Tonga",
"tt": "Trinidad and Tobago",
"tn": "Tunisia",
"tr": "Turkey",
"tm": "Turkmenistan",
"tc": "Turks and Caicos Islands",
"tv": "Tuvalu",
"ug": "Uganda",
"ua": "Ukraine",
"ae": "United Arab Emirates",
"gb": "United Kingdom",
"uk": "United Kingdom",
"um": "United States Minor Outlying Islands",
"us": "United States",
"uy": "Uruguay",
"vi": "US Virgin Islands",
"uz": "Uzbekistan",
"vu": "Vanuatu",
"va": "Vatican City State",
"ve": "Venezuela",
"vn": "Vietnam",
"wf": "Wallis and Futuna",
"eh": "Western Sahara",
"ye": "Yemen",
"zm": "Zambia",
"zw": "Zimbabwe",
}
}
-8
View File
@@ -1,8 +0,0 @@
package constants
const (
// TCP is a network protocol (reliable and slower than UDP).
TCP string = "tcp"
// UDP is a network protocol (unreliable and faster than TCP).
UDP string = "udp"
)
@@ -3,7 +3,9 @@ package providers
import (
"testing"
"github.com/qdm12/gluetun/pkg/updaters"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_All(t *testing.T) {
@@ -21,3 +23,27 @@ func Test_AllWithCustom(t *testing.T) {
assert.Contains(t, all, Custom)
assert.Len(t, all, len(All())+1)
}
func Test_MatchUpdaters(t *testing.T) {
t.Parallel()
fetcherNames := updaters.ListAllNames()
allProviders := All()
require.Equal(t, len(allProviders), len(fetcherNames), "number of providers in All() should match number of fetchers")
nameToFound := make(map[string]bool, len(allProviders))
for _, provider := range allProviders {
nameToFound[provider] = false
}
for _, fetcherName := range fetcherNames {
found, ok := nameToFound[fetcherName]
require.True(t, ok, "fetcher name %q not found in %v", fetcherName, allProviders)
require.False(t, found, "fetcher name %q is duplicated in %v", fetcherName, allProviders)
nameToFound[fetcherName] = true
}
for provider, found := range nameToFound {
assert.True(t, found, "provider %q from All() does not have a matching fetcher", provider)
}
}
-7
View File
@@ -1,7 +0,0 @@
package vpn
const (
AmneziaWg = "amneziawg"
OpenVPN = "openvpn"
Wireguard = "wireguard"
)
+6 -5
View File
@@ -5,7 +5,8 @@ import (
"strings"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/pkg/updaters/constants"
"github.com/qdm12/gluetun/pkg/updaters/models"
)
func boolToMarkdown(b bool) string {
@@ -42,7 +43,7 @@ const (
vpnHeader = "VPN"
)
func (s *Server) ToMarkdown(headers ...string) (markdown string) {
func serverToMarkdown(s models.Server, headers ...string) (markdown string) {
if len(headers) == 0 {
return ""
}
@@ -81,7 +82,7 @@ func (s *Server) ToMarkdown(headers ...string) (markdown string) {
case tcpHeader:
fields[i] = boolToMarkdown(s.TCP)
case udpHeader:
fields[i] = boolToMarkdown(s.UDP || s.VPN == vpn.Wireguard)
fields[i] = boolToMarkdown(s.UDP || s.VPN == constants.Wireguard)
case vpnHeader:
fields[i] = s.VPN
}
@@ -90,7 +91,7 @@ func (s *Server) ToMarkdown(headers ...string) (markdown string) {
return "| " + strings.Join(fields, " | ") + " |"
}
func (s *Servers) toMarkdown(vpnProvider string) (formatted string, err error) {
func serversToMarkdown(s models.Servers, vpnProvider string) (formatted string, err error) {
headers, err := getMarkdownHeaders(vpnProvider)
if err != nil {
return "", fmt.Errorf("getting markdown headers: %w", err)
@@ -100,7 +101,7 @@ func (s *Servers) toMarkdown(vpnProvider string) (formatted string, err error) {
entries := make([]string, len(s.Servers))
for i, server := range s.Servers {
entries[i] = server.ToMarkdown(headers...)
entries[i] = serverToMarkdown(server, headers...)
}
formatted = legend + "\n" +
+11 -10
View File
@@ -4,16 +4,17 @@ import (
"testing"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/pkg/updaters/constants"
"github.com/qdm12/gluetun/pkg/updaters/models"
"github.com/stretchr/testify/assert"
)
func Test_Servers_ToMarkdown(t *testing.T) {
func Test_serversToMarkdown(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
provider string
servers Servers
servers models.Servers
formatted string
errMessage string
}{
@@ -23,8 +24,8 @@ func Test_Servers_ToMarkdown(t *testing.T) {
},
providers.Cyberghost: {
provider: providers.Cyberghost,
servers: Servers{
Servers: []Server{
servers: models.Servers{
Servers: []models.Server{
{Country: "a", UDP: true, Hostname: "xa"},
{Country: "b", TCP: true, Hostname: "xb"},
},
@@ -36,10 +37,10 @@ func Test_Servers_ToMarkdown(t *testing.T) {
},
providers.Fastestvpn: {
provider: providers.Fastestvpn,
servers: Servers{
Servers: []Server{
{Country: "a", Hostname: "xa", VPN: vpn.OpenVPN, TCP: true},
{Country: "b", Hostname: "xb", VPN: vpn.OpenVPN, UDP: true},
servers: models.Servers{
Servers: []models.Server{
{Country: "a", Hostname: "xa", VPN: constants.OpenVPN, TCP: true},
{Country: "b", Hostname: "xb", VPN: constants.OpenVPN, UDP: true},
},
},
formatted: "| Country | Hostname | VPN | TCP | UDP |\n" +
@@ -53,7 +54,7 @@ func Test_Servers_ToMarkdown(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Parallel()
markdown, err := testCase.servers.toMarkdown(testCase.provider)
markdown, err := serversToMarkdown(testCase.servers, testCase.provider)
assert.Equal(t, testCase.formatted, markdown)
if testCase.errMessage != "" {
-32
View File
@@ -1,32 +0,0 @@
package models
import (
"net/netip"
)
type PublicIP struct {
IP netip.Addr `json:"public_ip"`
Region string `json:"region,omitempty"`
Country string `json:"country,omitempty"`
City string `json:"city,omitempty"`
Hostname string `json:"hostname,omitempty"`
Location string `json:"location,omitempty"`
Organization string `json:"organization,omitempty"`
PostalCode string `json:"postal_code,omitempty"`
Timezone string `json:"timezone,omitempty"`
}
func (p *PublicIP) Copy() (publicIPCopy PublicIP) {
publicIPCopy = PublicIP{
IP: p.IP,
Region: p.Region,
Country: p.Country,
City: p.City,
Hostname: p.Hostname,
Location: p.Location,
Organization: p.Organization,
PostalCode: p.PostalCode,
Timezone: p.Timezone,
}
return publicIPCopy
}
-93
View File
@@ -1,93 +0,0 @@
package models
import (
"errors"
"fmt"
"net/netip"
"reflect"
"strings"
"github.com/qdm12/gluetun/internal/constants/vpn"
)
type Server struct {
VPN string `json:"vpn,omitempty"`
// Surfshark: country is also used for multi-hop
Country string `json:"country,omitempty"`
Region string `json:"region,omitempty"`
City string `json:"city,omitempty"`
ISP string `json:"isp,omitempty"`
Categories []string `json:"categories,omitempty"`
Owned bool `json:"owned,omitempty"`
Number uint16 `json:"number,omitempty"`
ServerName string `json:"server_name,omitempty"`
Hostname string `json:"hostname,omitempty"`
TCP bool `json:"tcp,omitempty"`
UDP bool `json:"udp,omitempty"`
OvpnX509 string `json:"x509,omitempty"`
RetroLoc string `json:"retroloc,omitempty"` // TODO remove in v4
MultiHop bool `json:"multihop,omitempty"`
WgPubKey string `json:"wgpubkey,omitempty"`
Free bool `json:"free,omitempty"` // TODO v4 create a SubscriptionTier struct
Premium bool `json:"premium,omitempty"`
Stream bool `json:"stream,omitempty"` // TODO v4 create a Features struct
SecureCore bool `json:"secure_core,omitempty"`
Tor bool `json:"tor,omitempty"`
PortForward bool `json:"port_forward,omitempty"`
Keep bool `json:"keep,omitempty"`
IPs []netip.Addr `json:"ips,omitempty"`
}
func (s *Server) HasMinimumInformation() (err error) {
switch {
case s.VPN == "":
return errors.New("vpn field is empty")
case len(s.IPs) == 0:
return errors.New("ips field is empty")
case s.VPN == vpn.Wireguard && (s.TCP || s.UDP):
return errors.New("no network protocol should be set")
case s.VPN == vpn.OpenVPN && !s.TCP && !s.UDP:
return errors.New("both TCP and UDP fields are false for OpenVPN")
case s.VPN == vpn.Wireguard && s.WgPubKey == "":
return errors.New("wireguard public key field is empty")
default:
return nil
}
}
func (s *Server) Equal(other Server) (equal bool) {
if !ipsAreEqual(s.IPs, other.IPs) {
return false
}
serverCopy := *s
serverCopy.IPs = nil
other.IPs = nil
return reflect.DeepEqual(serverCopy, other)
}
func ipsAreEqual(a, b []netip.Addr) (equal bool) {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].Compare(b[i]) != 0 {
return false
}
}
return true
}
func (s *Server) Key() (key string) {
var protocols []string
if s.TCP {
protocols = append(protocols, "tcp")
}
if s.UDP {
protocols = append(protocols, "udp")
}
return fmt.Sprintf("%s-%s-%s", s.VPN, strings.Join(protocols, "-"), s.Hostname)
}
-125
View File
@@ -1,125 +0,0 @@
package models
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Server_Equal(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
a *Server
b Server
equal bool
}{
"same IPs": {
a: &Server{
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
},
b: Server{
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
},
equal: true,
},
"same IP strings": {
a: &Server{
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
},
b: Server{
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
},
equal: true,
},
"different IPs": {
a: &Server{
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4}), netip.AddrFrom4([4]byte{2, 3, 4, 5})},
},
b: Server{
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4}), netip.AddrFrom4([4]byte{1, 2, 3, 4})},
},
},
"all fields equal": {
a: &Server{
VPN: "vpn",
Country: "country",
Region: "region",
City: "city",
ISP: "isp",
Owned: true,
Number: 1,
ServerName: "server_name",
Hostname: "hostname",
TCP: true,
UDP: true,
OvpnX509: "x509",
RetroLoc: "retroloc",
MultiHop: true,
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
PortForward: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
Keep: true,
},
b: Server{
VPN: "vpn",
Country: "country",
Region: "region",
City: "city",
ISP: "isp",
Owned: true,
Number: 1,
ServerName: "server_name",
Hostname: "hostname",
TCP: true,
UDP: true,
OvpnX509: "x509",
RetroLoc: "retroloc",
MultiHop: true,
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
PortForward: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
Keep: true,
},
equal: true,
},
"different field": {
a: &Server{
VPN: "vpn",
},
b: Server{
VPN: "other vpn",
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ipsOfANotNil := testCase.a.IPs != nil
ipsOfBNotNil := testCase.b.IPs != nil
equal := testCase.a.Equal(testCase.b)
assert.Equal(t, testCase.equal, equal)
// Ensure IPs field is not modified
if ipsOfANotNil {
assert.NotNil(t, testCase.a)
}
if ipsOfBNotNil {
assert.NotNil(t, testCase.b)
}
})
}
}
+8 -13
View File
@@ -9,11 +9,12 @@ import (
"sort"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/pkg/updaters/models"
)
type AllServers struct {
Version uint16 // used for migration of the top level scheme
ProviderToServers map[string]Servers
ProviderToServers map[string]models.Servers
}
var _ json.Marshaler = (*AllServers)(nil)
@@ -110,7 +111,7 @@ func (a *AllServers) UnmarshalJSON(data []byte) (err error) {
return nil
}
a.ProviderToServers = make(map[string]Servers, len(keyValues))
a.ProviderToServers = make(map[string]models.Servers, len(keyValues))
allProviders := providers.All()
allProvidersSet := make(map[string]struct{}, len(allProviders))
@@ -131,7 +132,7 @@ func (a *AllServers) UnmarshalJSON(data []byte) (err error) {
key, err)
}
var servers Servers
var servers models.Servers
err = json.Unmarshal(jsonValue, &servers)
if err != nil {
return fmt.Errorf("decoding %s servers: %w",
@@ -151,24 +152,18 @@ func (a *AllServers) Count() (count int) {
return count
}
type Servers struct {
Version uint16 `json:"version"`
Timestamp int64 `json:"timestamp"`
Servers []Server `json:"servers,omitempty"`
}
func (s *Servers) Format(vpnProvider, format string) (formatted string, err error) {
func FormatServers(s models.Servers, vpnProvider, format string) (formatted string, err error) {
switch format {
case "markdown":
return s.toMarkdown(vpnProvider)
return serversToMarkdown(s, vpnProvider)
case "json":
return s.toJSON()
return serversToJSON(s)
default:
return "", fmt.Errorf("servers format not supported: %s", format)
}
}
func (s *Servers) toJSON() (formatted string, err error) {
func serversToJSON(s models.Servers) (formatted string, err error) {
buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
+11 -10
View File
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/pkg/updaters/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -20,18 +21,18 @@ func Test_AllServers_MarshalJSON(t *testing.T) {
}{
"no provider": {
allServers: &AllServers{
ProviderToServers: map[string]Servers{},
ProviderToServers: map[string]models.Servers{},
},
dataString: `{"version":0}`,
},
"two providers": {
allServers: &AllServers{
Version: 1,
ProviderToServers: map[string]Servers{
ProviderToServers: map[string]models.Servers{
providers.Cyberghost: {
Version: 1,
Timestamp: 1000,
Servers: []Server{
Servers: []models.Server{
{Country: "A"},
{Country: "B"},
},
@@ -39,7 +40,7 @@ func Test_AllServers_MarshalJSON(t *testing.T) {
providers.Privado: {
Version: 2,
Timestamp: 2000,
Servers: []Server{
Servers: []models.Server{
{City: "C"},
{City: "D"},
},
@@ -100,11 +101,11 @@ func Test_AllServers_UnmarshalJSON(t *testing.T) {
`"privado":{"version":2,"timestamp":2000,"servers":[{"city":"C"},{"city":"D"}]}}`,
allServers: AllServers{
Version: 1,
ProviderToServers: map[string]Servers{
ProviderToServers: map[string]models.Servers{
providers.Cyberghost: {
Version: 1,
Timestamp: 1000,
Servers: []Server{
Servers: []models.Server{
{Country: "A"},
{Country: "B"},
},
@@ -112,7 +113,7 @@ func Test_AllServers_UnmarshalJSON(t *testing.T) {
providers.Privado: {
Version: 2,
Timestamp: 2000,
Servers: []Server{
Servers: []models.Server{
{City: "C"},
{City: "D"},
},
@@ -146,11 +147,11 @@ func Test_AllServers_JSON_Marshal_Unmarshal(t *testing.T) {
allServers := &AllServers{
Version: 1,
ProviderToServers: map[string]Servers{
ProviderToServers: map[string]models.Servers{
providers.Cyberghost: {
Version: 1,
Timestamp: 1000,
Servers: []Server{
Servers: []models.Server{
{Country: "A"},
{Country: "B"},
},
@@ -158,7 +159,7 @@ func Test_AllServers_JSON_Marshal_Unmarshal(t *testing.T) {
providers.Privado: {
Version: 2,
Timestamp: 2000,
Servers: []Server{
Servers: []models.Server{
{City: "C"},
{City: "D"},
},
-42
View File
@@ -1,42 +0,0 @@
package models
import "sort"
var _ sort.Interface = (*SortableServers)(nil)
type SortableServers []Server
func (s SortableServers) Len() int {
return len(s)
}
func (s SortableServers) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s SortableServers) Less(i, j int) bool {
a, b := s[i], s[j]
if a.Country == b.Country { //nolint:nestif
if a.Region == b.Region {
if a.City == b.City {
if a.ServerName == b.ServerName {
if a.Number == b.Number {
if a.Hostname == b.Hostname {
if a.ISP == b.ISP {
return a.VPN < b.VPN
}
return a.ISP < b.ISP
}
return a.Hostname < b.Hostname
}
return a.Number < b.Number
}
return a.ServerName < b.ServerName
}
return a.City < b.City
}
return a.Region < b.Region
}
return a.Country < b.Country
}
+1 -1
View File
@@ -7,8 +7,8 @@ import (
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/pkg/updaters/constants"
)
func extractDataFromLines(lines []string) (
+1 -1
View File
@@ -5,8 +5,8 @@ import (
"net/netip"
"testing"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/pkg/updaters/constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+3 -4
View File
@@ -1,9 +1,8 @@
package pmtud
import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/vpn"
pconstants "github.com/qdm12/gluetun/internal/pmtud/constants"
"github.com/qdm12/gluetun/pkg/updaters/constants"
)
// MaxTheoreticalVPNMTU returns the theoretical maximum MTU for a VPN tunnel
@@ -27,9 +26,9 @@ func MaxTheoreticalVPNMTU(vpnType, network string, ipv6 bool) uint32 {
panic("unknown network protocol: " + network)
}
switch vpnType {
case vpn.Wireguard, vpn.AmneziaWg:
case constants.Wireguard, constants.AmneziaWg:
vpnLinkMTU -= pconstants.WireguardHeaderLength
case vpn.OpenVPN:
case constants.OpenVPN:
vpnLinkMTU -= pconstants.OpenVPNHeaderMaxLength
default:
panic("unknown VPN type: " + vpnType)
+4 -3
View File
@@ -4,22 +4,23 @@ import (
"net/http"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/airvpn/updater"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/airvpn"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
func New(storage common.Storage, client *http.Client) *Provider {
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(client),
Fetcher: airvpn.New(client),
}
}
-64
View File
@@ -1,64 +0,0 @@
package updater
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/netip"
)
type apiData struct {
Servers []apiServer `json:"servers"`
}
type apiServer struct {
PublicName string `json:"public_name"`
CountryName string `json:"country_name"`
CountryCode string `json:"country_code"`
Location string `json:"location"`
Continent string `json:"continent"`
IPv4In1 netip.Addr `json:"ip_v4_in1"`
IPv4In2 netip.Addr `json:"ip_v4_in2"`
IPv4In3 netip.Addr `json:"ip_v4_in3"`
IPv4In4 netip.Addr `json:"ip_v4_in4"`
IPv6In1 netip.Addr `json:"ip_v6_in1"`
IPv6In2 netip.Addr `json:"ip_v6_in2"`
IPv6In3 netip.Addr `json:"ip_v6_in3"`
IPv6In4 netip.Addr `json:"ip_v6_in4"`
Health string `json:"health"`
}
func fetchAPI(ctx context.Context, client *http.Client) (
data apiData, err error,
) {
const url = "https://airvpn.org/api/status/"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return data, fmt.Errorf("creating HTTP request: %w", err)
}
response, err := client.Do(request)
if err != nil {
return data, fmt.Errorf("doing HTTP request: %w", err)
}
if response.StatusCode != http.StatusOK {
_ = response.Body.Close()
return data, fmt.Errorf("HTTP status code not OK: %d %s",
response.StatusCode, response.Status)
}
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&data); err != nil {
_ = response.Body.Close()
return data, fmt.Errorf("decoding response body: %w", err)
}
if err := response.Body.Close(); err != nil {
return data, fmt.Errorf("closing response body: %w", err)
}
return data, nil
}
-101
View File
@@ -1,101 +0,0 @@
package updater
import (
"context"
"fmt"
"net/netip"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/constants/vpn"
"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,
) {
data, err := fetchAPI(ctx, u.client)
if err != nil {
return nil, fmt.Errorf("fetching API: %w", err)
}
// every API server model has:
// - Wireguard server using IPv4In1
// - Wiregard server using IPv6In1
// - OpenVPN TCP+UDP+SSH+SSL server with tls-auth using IPv4In1 and IPv6In1
// - OpenVPN TCP+UDP+SSH+SSL server with tls-auth using IPv4In2 and IPv6In2
// - OpenVPN TCP+UDP+SSH+SSL server with tls-crypt using IPv4In3 and IPv6In3
// - OpenVPN TCP+UDP+SSH+SSL server with tls-crypt using IPv6In4 and IPv6In4
const numberOfServersPerAPIServer = 1 + // Wireguard server using IPv4In1
1 + // Wiregard server using IPv6In1
4 + // OpenVPN TCP server with tls-auth using IPv4In3, IPv6In3, IPv4In4, IPv6In4
4 // OpenVPN UDP server with tls-auth using IPv4In3, IPv6In3, IPv4In4, IPv6In4
projectedNumberOfServers := numberOfServersPerAPIServer * len(data.Servers)
if projectedNumberOfServers < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, projectedNumberOfServers, minServers)
}
servers = make([]models.Server, 0, projectedNumberOfServers)
for _, apiServer := range data.Servers {
if apiServer.Health != "ok" {
continue
}
city := strings.ReplaceAll(apiServer.Location, ", ", " ")
city = strings.ReplaceAll(city, ",", "")
baseServer := models.Server{
ServerName: apiServer.PublicName,
Country: apiServer.CountryName,
City: city,
Region: apiServer.Continent,
}
baseWireguardServer := baseServer
baseWireguardServer.VPN = vpn.Wireguard
baseWireguardServer.WgPubKey = "PyLCXAQT8KkM4T+dUsOQfn+Ub3pGxfGlxkIApuig+hk="
ipv4WireguadServer := baseWireguardServer
ipv4WireguadServer.IPs = []netip.Addr{apiServer.IPv4In1}
ipv4WireguadServer.Hostname = apiServer.CountryCode + ".vpn.airdns.org"
servers = append(servers, ipv4WireguadServer)
ipv6WireguadServer := baseWireguardServer
ipv6WireguadServer.IPs = []netip.Addr{apiServer.IPv6In1}
ipv6WireguadServer.Hostname = apiServer.CountryCode + ".ipv6.vpn.airdns.org"
servers = append(servers, ipv6WireguadServer)
baseOpenVPNServer := baseServer
baseOpenVPNServer.VPN = vpn.OpenVPN
baseOpenVPNServer.UDP = true
baseOpenVPNServer.TCP = true
// Ignore IPs 1 and 2 since tls-crypt is superior to tls-auth really.
ipv4In3OpenVPNServer := baseOpenVPNServer
ipv4In3OpenVPNServer.IPs = []netip.Addr{apiServer.IPv4In3}
ipv4In3OpenVPNServer.Hostname = apiServer.CountryCode + "3.vpn.airdns.org"
servers = append(servers, ipv4In3OpenVPNServer)
ipv6In3OpenVPNServer := baseOpenVPNServer
ipv6In3OpenVPNServer.IPs = []netip.Addr{apiServer.IPv6In3}
ipv6In3OpenVPNServer.Hostname = apiServer.CountryCode + "3.ipv6.vpn.airdns.org"
servers = append(servers, ipv6In3OpenVPNServer)
ipv4In4OpenVPNServer := baseOpenVPNServer
ipv4In4OpenVPNServer.IPs = []netip.Addr{apiServer.IPv4In4}
ipv4In4OpenVPNServer.Hostname = apiServer.CountryCode + "4.vpn.airdns.org"
servers = append(servers, ipv4In4OpenVPNServer)
ipv6In4OpenVPNServer := baseOpenVPNServer
ipv6In4OpenVPNServer.IPs = []netip.Addr{apiServer.IPv6In4}
ipv6In4OpenVPNServer.Hostname = apiServer.CountryCode + "4.ipv6.vpn.airdns.org"
servers = append(servers, ipv6In4OpenVPNServer)
}
sort.Sort(models.SortableServers(servers))
return servers, nil
}
@@ -1,15 +0,0 @@
package updater
import (
"net/http"
)
type Updater struct {
client *http.Client
}
func New(client *http.Client) *Updater {
return &Updater{
client: client,
}
}
+2 -117
View File
@@ -1,59 +1,17 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/provider/common (interfaces: ParallelResolver,Storage,Unzipper,Warner)
// Source: github.com/qdm12/gluetun/internal/provider/common (interfaces: Storage)
// Package common is a generated GoMock package.
package common
import (
context "context"
netip "net/netip"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
settings "github.com/qdm12/gluetun/internal/configuration/settings"
models "github.com/qdm12/gluetun/internal/models"
resolver "github.com/qdm12/gluetun/internal/updater/resolver"
models "github.com/qdm12/gluetun/pkg/updaters/models"
)
// MockParallelResolver is a mock of ParallelResolver interface.
type MockParallelResolver struct {
ctrl *gomock.Controller
recorder *MockParallelResolverMockRecorder
}
// MockParallelResolverMockRecorder is the mock recorder for MockParallelResolver.
type MockParallelResolverMockRecorder struct {
mock *MockParallelResolver
}
// NewMockParallelResolver creates a new mock instance.
func NewMockParallelResolver(ctrl *gomock.Controller) *MockParallelResolver {
mock := &MockParallelResolver{ctrl: ctrl}
mock.recorder = &MockParallelResolverMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockParallelResolver) EXPECT() *MockParallelResolverMockRecorder {
return m.recorder
}
// Resolve mocks base method.
func (m *MockParallelResolver) Resolve(arg0 context.Context, arg1 resolver.ParallelSettings) (map[string][]netip.Addr, []string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Resolve", arg0, arg1)
ret0, _ := ret[0].(map[string][]netip.Addr)
ret1, _ := ret[1].([]string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Resolve indicates an expected call of Resolve.
func (mr *MockParallelResolverMockRecorder) Resolve(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockParallelResolver)(nil).Resolve), arg0, arg1)
}
// MockStorage is a mock of Storage interface.
type MockStorage struct {
ctrl *gomock.Controller
@@ -91,76 +49,3 @@ func (mr *MockStorageMockRecorder) FilterServers(arg0, arg1 interface{}) *gomock
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterServers", reflect.TypeOf((*MockStorage)(nil).FilterServers), arg0, arg1)
}
// MockUnzipper is a mock of Unzipper interface.
type MockUnzipper struct {
ctrl *gomock.Controller
recorder *MockUnzipperMockRecorder
}
// MockUnzipperMockRecorder is the mock recorder for MockUnzipper.
type MockUnzipperMockRecorder struct {
mock *MockUnzipper
}
// NewMockUnzipper creates a new mock instance.
func NewMockUnzipper(ctrl *gomock.Controller) *MockUnzipper {
mock := &MockUnzipper{ctrl: ctrl}
mock.recorder = &MockUnzipperMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUnzipper) EXPECT() *MockUnzipperMockRecorder {
return m.recorder
}
// FetchAndExtract mocks base method.
func (m *MockUnzipper) FetchAndExtract(arg0 context.Context, arg1 string) (map[string][]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchAndExtract", arg0, arg1)
ret0, _ := ret[0].(map[string][]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchAndExtract indicates an expected call of FetchAndExtract.
func (mr *MockUnzipperMockRecorder) FetchAndExtract(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAndExtract", reflect.TypeOf((*MockUnzipper)(nil).FetchAndExtract), arg0, arg1)
}
// MockWarner is a mock of Warner interface.
type MockWarner struct {
ctrl *gomock.Controller
recorder *MockWarnerMockRecorder
}
// MockWarnerMockRecorder is the mock recorder for MockWarner.
type MockWarnerMockRecorder struct {
mock *MockWarner
}
// NewMockWarner creates a new mock instance.
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
mock := &MockWarner{ctrl: ctrl}
mock.recorder = &MockWarnerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
return m.recorder
}
// Warn mocks base method.
func (m *MockWarner) Warn(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Warn", arg0)
}
// Warn indicates an expected call of Warn.
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
}
@@ -3,4 +3,4 @@ package common
// Exceptionally, these mocks are exported since they are used by all
// provider subpackages tests, and it reduces test code duplication a lot.
// Note mocks.go might need to be removed before re-generating it.
//go:generate mockgen -destination=mocks.go -package $GOPACKAGE . ParallelResolver,Storage,Unzipper,Warner
//go:generate mockgen -destination=mocks.go -package $GOPACKAGE . Storage
+1 -1
View File
@@ -2,7 +2,7 @@ package common
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/pkg/updaters/models"
)
type Storage interface {
-39
View File
@@ -1,39 +0,0 @@
package common
import (
"context"
"errors"
"net/netip"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
var (
ErrNotEnoughServers = errors.New("not enough servers found")
ErrCredentialsMissing = errors.New("credentials are missing")
)
type Fetcher interface {
FetchServers(ctx context.Context, minServers int) (servers []models.Server, err error)
}
type ParallelResolver interface {
Resolve(ctx context.Context, settings resolver.ParallelSettings) (
hostToIPs map[string][]netip.Addr, warnings []string, err error)
}
type Unzipper interface {
FetchAndExtract(ctx context.Context, url string) (
contents map[string][]byte, err error)
}
type Warner interface {
Warn(s string)
}
type IPFetcher interface {
String() string
CanFetchAnyIP() bool
FetchInfo(ctx context.Context, ip netip.Addr) (result models.PublicIP, err error)
}
+4 -5
View File
@@ -4,9 +4,8 @@ import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/pkg/updaters/constants"
)
// GetConnection gets the connection from the OpenVPN configuration file.
@@ -14,9 +13,9 @@ func (p *Provider) GetConnection(selection settings.ServerSelection, _ bool) (
connection models.Connection, err error,
) {
switch selection.VPN {
case vpn.OpenVPN:
case constants.OpenVPN:
return getOpenVPNConnection(p.extractor, selection)
case vpn.Wireguard, vpn.AmneziaWg:
case constants.Wireguard, constants.AmneziaWg:
return getWireguardConnection(selection), nil
default:
return connection, fmt.Errorf("VPN type not supported for custom provider: %s", selection.VPN)
@@ -52,7 +51,7 @@ func getWireguardConnection(selection settings.ServerSelection) (
connection models.Connection,
) {
connection = models.Connection{
Type: vpn.Wireguard,
Type: constants.Wireguard,
IP: selection.Wireguard.EndpointIP,
Port: *selection.Wireguard.EndpointPort,
Protocol: constants.UDP,
+1 -1
View File
@@ -5,9 +5,9 @@ import (
"testing"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/pkg/updaters/constants"
"github.com/stretchr/testify/assert"
)
+1 -1
View File
@@ -2,8 +2,8 @@ package custom
import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/utils"
"github.com/qdm12/gluetun/pkg/updaters/common"
)
type Provider struct {
+6 -5
View File
@@ -3,23 +3,24 @@ package cyberghost
import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/cyberghost/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/cyberghost"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
func New(storage common.Storage, updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
func New(storage common.Storage, updaterWarner updaters.Warner,
parallelResolver updaters.ParallelResolver,
) *Provider {
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(parallelResolver, updaterWarner),
Fetcher: cyberghost.New(parallelResolver, updaterWarner),
}
}
@@ -1,267 +0,0 @@
package updater
import "github.com/qdm12/gluetun/internal/constants"
func getGroupIDToProtocol() map[string]string {
return map[string]string{
"87-1": constants.UDP, // Premium UDP
"87-8": constants.UDP, // NoSpy UDP
"87-19": constants.UDP, // Gaming UDP
"97-1": constants.TCP, // Premium TCP
"97-8": constants.TCP, // NoSpy TCP
"97-19": constants.TCP, // Gaming TCP
}
}
func getSubdomainToRegion() map[string]string {
return map[string]string{
"af": "Afghanistan",
"ax": "Aland Islands",
"al": "Albania",
"dz": "Algeria",
"as": "American Samoa",
"ad": "Andorra",
"ao": "Angola",
"ai": "Anguilla",
"aq": "Antarctica",
"ag": "Antigua and Barbuda",
"ar": "Argentina",
"am": "Armenia",
"aw": "Aruba",
"au": "Australia",
"at": "Austria",
"az": "Azerbaijan",
"bs": "Bahamas",
"bh": "Bahrain",
"bd": "Bangladesh",
"bb": "Barbados",
"by": "Belarus",
"be": "Belgium",
"bz": "Belize",
"bj": "Benin",
"bm": "Bermuda",
"bt": "Bhutan",
"bo": "Bolivia",
"bq": "Bonaire",
"ba": "Bosnia and Herzegovina",
"bw": "Botswana",
"bv": "Bouvet Island",
"br": "Brazil",
"io": "British Indian Ocean Territory",
"vg": "British Virgin Islands",
"bn": "Brunei Darussalam",
"bg": "Bulgaria",
"bf": "Burkina Faso",
"bi": "Burundi",
"kh": "Cambodia",
"cm": "Cameroon",
"ca": "Canada",
"cv": "Cape Verde",
"ky": "Cayman Islands",
"cf": "Central African Republic",
"td": "Chad",
"cl": "Chile",
"cn": "China",
"cx": "Christmas Island",
"cc": "Cocos Islands",
"co": "Colombia",
"km": "Comoros",
"cg": "Congo",
"ck": "Cook Islands",
"cr": "Costa Rica",
"ci": "Cote d'Ivoire",
"hr": "Croatia",
"cu": "Cuba",
"cw": "Curacao",
"cy": "Cyprus",
"cz": "Czech Republic",
"cd": "Democratic Republic of the Congo",
"dk": "Denmark",
"dj": "Djibouti",
"dm": "Dominica",
"do": "Dominican Republic",
"ec": "Ecuador",
"eg": "Egypt",
"sv": "El Salvador",
"gq": "Equatorial Guinea",
"er": "Eritrea",
"ee": "Estonia",
"et": "Ethiopia",
"fk": "Falkland Islands",
"fo": "Faroe Islands",
"fj": "Fiji",
"fi": "Finland",
"fr": "France",
"gf": "French Guiana",
"pf": "French Polynesia",
"tf": "French Southern Territories",
"ga": "Gabon",
"gm": "Gambia",
"ge": "Georgia",
"de": "Germany",
"gh": "Ghana",
"gi": "Gibraltar",
"gr": "Greece",
"gl": "Greenland",
"gd": "Grenada",
"gp": "Guadeloupe",
"gu": "Guam",
"gt": "Guatemala",
"gg": "Guernsey",
"gw": "Guinea-Bissau",
"gn": "Guinea",
"gy": "Guyana",
"ht": "Haiti",
"hm": "Heard Island and McDonald Islands",
"hn": "Honduras",
"hk": "Hong Kong",
"hu": "Hungary",
"is": "Iceland",
"in": "India",
"id": "Indonesia",
"ir": "Iran",
"iq": "Iraq",
"ie": "Ireland",
"im": "Isle of Man",
"il": "Israel",
"it": "Italy",
"jm": "Jamaica",
"jp": "Japan",
"je": "Jersey",
"jo": "Jordan",
"kz": "Kazakhstan",
"ke": "Kenya",
"ki": "Kiribati",
"kr": "Korea",
"kw": "Kuwait",
"kg": "Kyrgyzstan",
"la": "Lao People's Democratic Republic",
"lv": "Latvia",
"lb": "Lebanon",
"ls": "Lesotho",
"lr": "Liberia",
"ly": "Libya",
"li": "Liechtenstein",
"lt": "Lithuania",
"lu": "Luxembourg",
"mo": "Macao",
"mk": "Macedonia",
"mg": "Madagascar",
"mw": "Malawi",
"my": "Malaysia",
"mv": "Maldives",
"ml": "Mali",
"mt": "Malta",
"mh": "Marshall Islands",
"mq": "Martinique",
"mr": "Mauritania",
"mu": "Mauritius",
"yt": "Mayotte",
"mx": "Mexico",
"fm": "Micronesia",
"md": "Moldova",
"mc": "Monaco",
"mn": "Mongolia",
"me": "Montenegro",
"ms": "Montserrat",
"ma": "Morocco",
"mz": "Mozambique",
"mm": "Myanmar",
"na": "Namibia",
"nr": "Nauru",
"np": "Nepal",
"nl": "Netherlands",
"nc": "New Caledonia",
"nz": "New Zealand",
"ni": "Nicaragua",
"ne": "Niger",
"ng": "Nigeria",
"nu": "Niue",
"nf": "Norfolk Island",
"mp": "Northern Mariana Islands",
"no": "Norway",
"om": "Oman",
"pk": "Pakistan",
"pw": "Palau",
"ps": "Palestine, State of",
"pa": "Panama",
"pg": "Papua New Guinea",
"py": "Paraguay",
"pe": "Peru",
"ph": "Philippines",
"pn": "Pitcairn",
"pl": "Poland",
"pt": "Portugal",
"pr": "Puerto Rico",
"qa": "Qatar",
"re": "Reunion",
"ro": "Romania",
"ru": "Russian Federation",
"rw": "Rwanda",
"bl": "Saint Barthelemy",
"sh": "Saint Helena",
"kn": "Saint Kitts and Nevis",
"lc": "Saint Lucia",
"mf": "Saint Martin",
"pm": "Saint Pierre and Miquelon",
"vc": "Saint Vincent and the Grenadines",
"ws": "Samoa",
"sm": "San Marino",
"st": "Sao Tome and Principe",
"sa": "Saudi Arabia",
"sn": "Senegal",
"rs": "Serbia",
"sc": "Seychelles",
"sl": "Sierra Leone",
"sg": "Singapore",
"sx": "Sint Maarten",
"sk": "Slovakia",
"si": "Slovenia",
"sb": "Solomon Islands",
"so": "Somalia",
"za": "South Africa",
"gs": "South Georgia and the South Sandwich Islands",
"ss": "South Sudan",
"es": "Spain",
"lk": "Sri Lanka",
"sd": "Sudan",
"sr": "Suriname",
"sj": "Svalbard and Jan Mayen",
"sz": "Swaziland",
"se": "Sweden",
"ch": "Switzerland",
"sy": "Syrian Arab Republic",
"tw": "Taiwan",
"tj": "Tajikistan",
"tz": "Tanzania",
"th": "Thailand",
"tl": "Timor-Leste",
"tg": "Togo",
"tk": "Tokelau",
"to": "Tonga",
"tt": "Trinidad and Tobago",
"tn": "Tunisia",
"tr": "Turkey",
"tm": "Turkmenistan",
"tc": "Turks and Caicos Islands",
"tv": "Tuvalu",
"ug": "Uganda",
"ua": "Ukraine",
"ae": "United Arab Emirates",
"gb": "United Kingdom",
"um": "United States Minor Outlying Islands",
"us": "United States",
"uy": "Uruguay",
"vi": "US Virgin Islands",
"uz": "Uzbekistan",
"vu": "Vanuatu",
"va": "Vatican City State",
"ve": "Venezuela",
"vn": "Vietnam",
"wf": "Wallis and Futuna",
"eh": "Western Sahara",
"ye": "Yemen",
"zm": "Zambia",
"zw": "Zimbabwe",
}
}
@@ -1,15 +0,0 @@
package updater
func mergeCountryCodes(base, extend map[string]string) (merged map[string]string) {
merged = make(map[string]string, len(base))
for countryCode, region := range base {
merged[countryCode] = region
}
for countryCode := range base {
delete(extend, countryCode)
}
for countryCode, region := range extend {
merged[countryCode] = region
}
return merged
}
@@ -1,69 +0,0 @@
package updater
import (
"net/netip"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
)
type hostToServer map[string]models.Server
func getPossibleServers() (possibleServers hostToServer) {
groupIDToProtocol := getGroupIDToProtocol()
cyberghostCountryCodes := getSubdomainToRegion()
allCountryCodes := constants.CountryCodes()
possibleCountryCodes := mergeCountryCodes(cyberghostCountryCodes, allCountryCodes)
n := len(groupIDToProtocol) * len(possibleCountryCodes)
possibleServers = make(hostToServer, n) // key is the host
for groupID, protocol := range groupIDToProtocol {
for countryCode, country := range possibleCountryCodes {
const domain = "cg-dialup.net"
possibleHost := groupID + "-" + countryCode + "." + domain
possibleServer := models.Server{
VPN: vpn.OpenVPN,
Hostname: possibleHost,
Country: country,
TCP: protocol == constants.TCP,
UDP: protocol == constants.UDP,
}
possibleServers[possibleHost] = possibleServer
}
}
return possibleServers
}
func (hts hostToServer) hostsSlice() (hosts []string) {
hosts = make([]string, 0, len(hts))
for host := range hts {
hosts = append(hosts, host)
}
return hosts
}
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]netip.Addr) {
for host, IPs := range hostToIPs {
server := hts[host]
server.IPs = IPs
hts[host] = server
}
for host, server := range hts {
if len(server.IPs) == 0 {
delete(hts, host)
}
}
}
func (hts hostToServer) toSlice() (servers []models.Server) {
servers = make([]models.Server, 0, len(hts))
for _, server := range hts {
servers = append(servers, server)
}
return servers
}
@@ -1,28 +0,0 @@
package updater
import (
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
const (
maxFailRatio = 1
maxDuration = 20 * time.Second
betweenDuration = time.Second
maxNoNew = 4
maxFails = 10
)
return resolver.ParallelSettings{
Hosts: hosts,
MaxFailRatio: maxFailRatio,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
BetweenDuration: betweenDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
}
@@ -1,43 +0,0 @@
package updater
import (
"context"
"fmt"
"sort"
"strings"
"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,
) {
possibleServers := getPossibleServers()
possibleHosts := possibleServers.hostsSlice()
resolveSettings := parallelResolverSettings(possibleHosts)
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
for _, warning := range warnings {
if strings.HasSuffix(warning, "no such host") {
continue // ignore no such host warnings
}
u.warner.Warn(warning)
}
if err != nil {
return nil, err
}
if len(hostToIPs) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
possibleServers.adaptWithIPs(hostToIPs)
servers = possibleServers.toSlice()
sort.Sort(models.SortableServers(servers))
return servers, nil
}
@@ -1,17 +0,0 @@
package updater
import (
"github.com/qdm12/gluetun/internal/provider/common"
)
type Updater struct {
parallelResolver common.ParallelResolver
warner common.Warner
}
func New(parallelResolver common.ParallelResolver, warner common.Warner) *Updater {
return &Updater{
parallelResolver: parallelResolver,
warner: warner,
}
}
+6 -5
View File
@@ -5,24 +5,25 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/example/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/example"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
// TODO: remove unneeded arguments once the updater is implemented.
func New(storage common.Storage, updaterWarner common.Warner, client *http.Client,
unzipper common.Unzipper, parallelResolver common.ParallelResolver,
func New(storage common.Storage, updaterWarner updaters.Warner, client *http.Client,
unzipper updaters.Unzipper, parallelResolver updaters.ParallelResolver,
) *Provider {
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(updaterWarner, unzipper, client, parallelResolver),
Fetcher: example.New(updaterWarner, unzipper, client, parallelResolver),
}
}
-57
View File
@@ -1,57 +0,0 @@
package updater
import (
"context"
"encoding/json"
"fmt"
"net/http"
)
type apiData struct {
Servers []apiServer `json:"servers"`
}
type apiServer struct {
OpenVPNHostname string `json:"openvpn_hostname"`
WireguardHostname string `json:"wireguard_hostname"`
Country string `json:"country"`
Region string `json:"region"`
City string `json:"city"`
WgPubKey string `json:"wg_public_key"`
}
func fetchAPI(ctx context.Context, client *http.Client) (
data apiData, err error,
) {
// TODO: adapt this URL and the structures above to match the real
// API models you have.
const url = "https://example.com/servers"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return data, err
}
response, err := client.Do(request)
if err != nil {
return data, err
}
if response.StatusCode != http.StatusOK {
_ = response.Body.Close()
return data, fmt.Errorf("HTTP status code not OK: %d %s",
response.StatusCode, response.Status)
}
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&data); err != nil {
_ = response.Body.Close()
return data, fmt.Errorf("decoding response body: %w", err)
}
if err := response.Body.Close(); err != nil {
return data, fmt.Errorf("closing response body: %w", err)
}
return data, nil
}
@@ -1,32 +0,0 @@
package updater
import (
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
// TODO: remove this file if the parallel resolver is not used
// by the updater.
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
// TODO: adapt these constant values below to make the resolution
// as fast and as reliable as possible.
const (
maxFailRatio = 0.1
maxDuration = 20 * time.Second
betweenDuration = time.Second
maxNoNew = 2
maxFails = 2
)
return resolver.ParallelSettings{
Hosts: hosts,
MaxFailRatio: maxFailRatio,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
BetweenDuration: betweenDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
}
@@ -1,125 +0,0 @@
package updater
import (
"context"
"fmt"
"sort"
"github.com/qdm12/gluetun/internal/constants/vpn"
"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,
) {
// FetchServers obtains information for each VPN server
// for the VPN service provider.
//
// You should aim at obtaining as much information as possible
// for each server, such as their location information.
// Required fields for each server are:
// - the `VPN` protocol string field
// - the `Hostname` string field
// - the `IPs` IP slice field
// - have one network protocol set, either `TCP` or `UDP`
// - If `VPN` is `wireguard`, the `WgPubKey` field to be set
//
// The information obtention can be done in different ways
// or by combining ways, depending on how the provider exposes
// this information. Some common ones are listed below:
//
// - you can use u.client to fetch structured (usually JSON)
// data of the servers from an HTTP API endpoint of the provider.
// Example in: `internal/provider/mullvad/updater`
// - you can use u.unzipper to download, unzip and parse a zip
// file of OpenVPN configuration files.
// Example in: `internal/provider/fastestvpn/updater`
// - you can use u.parallelResolver to resolve all hostnames
// found in parallel to obtain their corresponding IP addresses.
// Example in: `internal/provider/fastestvpn/updater`
//
// The following is an example code which fetches server
// information from an HTTP API endpoint of the provider,
// and then resolves in parallel all hostnames to get their
// IP addresses. You should pay attention to the following:
// - we check multiple times we have enough servers
// before continuing processing.
// - hosts are deduplicated to reduce parallel resolution
// load.
// - servers are sorted at the end.
//
// Once you are done, please check all the TODO comments
// in this package and address them.
data, err := fetchAPI(ctx, u.client)
if err != nil {
return nil, fmt.Errorf("fetching API: %w", err)
}
uniqueHosts := make(map[string]struct{}, len(data.Servers))
for _, serverData := range data.Servers {
if serverData.OpenVPNHostname != "" {
uniqueHosts[serverData.OpenVPNHostname] = struct{}{}
}
if serverData.WireguardHostname != "" {
uniqueHosts[serverData.WireguardHostname] = struct{}{}
}
}
if len(uniqueHosts) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(uniqueHosts), minServers)
}
hosts := make([]string, 0, len(uniqueHosts))
for host := range uniqueHosts {
hosts = append(hosts, host)
}
resolveSettings := parallelResolverSettings(hosts)
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
for _, warning := range warnings {
u.warner.Warn(warning)
}
if err != nil {
return nil, fmt.Errorf("resolving hosts: %w", err)
}
if len(hostToIPs) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
maxServers := 2 * len(data.Servers) //nolint:mnd
servers = make([]models.Server, 0, maxServers)
for _, serverData := range data.Servers {
baseServer := models.Server{
Country: serverData.Country,
Region: serverData.Region,
City: serverData.City,
WgPubKey: serverData.WgPubKey,
}
if serverData.OpenVPNHostname != "" {
openvpnServer := baseServer
openvpnServer.VPN = vpn.OpenVPN
openvpnServer.UDP = true
openvpnServer.TCP = true
openvpnServer.Hostname = serverData.OpenVPNHostname
openvpnServer.IPs = hostToIPs[serverData.OpenVPNHostname]
servers = append(servers, openvpnServer)
}
if serverData.WireguardHostname != "" {
wireguardServer := baseServer
wireguardServer.VPN = vpn.Wireguard
wireguardServer.Hostname = serverData.WireguardHostname
wireguardServer.IPs = hostToIPs[serverData.WireguardHostname]
servers = append(servers, wireguardServer)
}
}
sort.Sort(models.SortableServers(servers))
return servers, nil
}
@@ -1,27 +0,0 @@
package updater
import (
"net/http"
"github.com/qdm12/gluetun/internal/provider/common"
)
type Updater struct {
// TODO: remove fields not used by the updater
client *http.Client
unzipper common.Unzipper
parallelResolver common.ParallelResolver
warner common.Warner
}
func New(warner common.Warner, unzipper common.Unzipper,
client *http.Client, parallelResolver common.ParallelResolver,
) *Updater {
// TODO: remove arguments not used by the updater
return &Updater{
client: client,
unzipper: unzipper,
parallelResolver: parallelResolver,
warner: warner,
}
}
+12 -11
View File
@@ -7,11 +7,12 @@ import (
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/constants"
umodels "github.com/qdm12/gluetun/pkg/updaters/models"
"github.com/stretchr/testify/assert"
)
@@ -21,7 +22,7 @@ func Test_Provider_GetConnection(t *testing.T) {
const provider = providers.Expressvpn
testCases := map[string]struct {
filteredServers []models.Server
filteredServers []umodels.Server
storageErr error
selection settings.ServerSelection
ipv6Supported bool
@@ -34,7 +35,7 @@ func Test_Provider_GetConnection(t *testing.T) {
errMessage: "filtering servers: test error",
},
"default OpenVPN TCP port": {
filteredServers: []models.Server{
filteredServers: []umodels.Server{
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}},
},
selection: settings.ServerSelection{
@@ -45,7 +46,7 @@ func Test_Provider_GetConnection(t *testing.T) {
panicMessage: "no default OpenVPN TCP port is defined!",
},
"default OpenVPN UDP port": {
filteredServers: []models.Server{
filteredServers: []umodels.Server{
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}},
},
selection: settings.ServerSelection{
@@ -54,18 +55,18 @@ func Test_Provider_GetConnection(t *testing.T) {
},
}.WithDefaults(provider),
connection: models.Connection{
Type: vpn.OpenVPN,
Type: constants.OpenVPN,
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
Port: 1195,
Protocol: constants.UDP,
},
},
"default Wireguard port": {
filteredServers: []models.Server{
filteredServers: []umodels.Server{
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}},
},
selection: settings.ServerSelection{
VPN: vpn.Wireguard,
VPN: constants.Wireguard,
}.WithDefaults(provider),
panicMessage: "no default Wireguard port is defined!",
},
@@ -80,9 +81,9 @@ func Test_Provider_GetConnection(t *testing.T) {
storage.EXPECT().FilterServers(provider, testCase.selection).
Return(testCase.filteredServers, testCase.storageErr)
unzipper := (common.Unzipper)(nil)
warner := (common.Warner)(nil)
parallelResolver := (common.ParallelResolver)(nil)
unzipper := (updaters.Unzipper)(nil)
warner := (updaters.Warner)(nil)
parallelResolver := (updaters.ParallelResolver)(nil)
provider := New(storage, unzipper, warner, parallelResolver)
if testCase.panicMessage != "" {
+6 -5
View File
@@ -3,23 +3,24 @@ package expressvpn
import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/expressvpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/expressvpn"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
func New(storage common.Storage, unzipper updaters.Unzipper, updaterWarner updaters.Warner,
parallelResolver updaters.ParallelResolver,
) *Provider {
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
Fetcher: expressvpn.New(unzipper, updaterWarner, parallelResolver),
}
}
@@ -1,180 +0,0 @@
package updater
import (
"github.com/qdm12/gluetun/internal/models"
)
func hardcodedServers() (servers []models.Server) {
return []models.Server{
{Country: "Albania", Hostname: "albania-ca-version-2.expressnetw.com"},
{Country: "Algeria", Hostname: "algeria-ca-version-2.expressnetw.com"},
{Country: "Andorra", Hostname: "andorra-ca-version-2.expressnetw.com"},
{Country: "Argentina", Hostname: "argentina-ca-version-2.expressnetw.com"},
{Country: "Armenia", Hostname: "armenia-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Adelaide", Hostname: "australia-adelaide--ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Brisbane", Hostname: "australia-brisbane-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Melbourne", Hostname: "australia-melbourne-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Perth", Hostname: "australia-perth-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Sydney", Hostname: "australia-sydney-2-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Sydney", Hostname: "australia-sydney-ca-version-2.expressnetw.com"},
{Country: "Australia", City: "Woolloomooloo", Hostname: "australia-woolloomooloo-2-ca-version-2.expressnetw.com"},
{Country: "Austria", Hostname: "austria-ca-version-2.expressnetw.com"},
{Country: "Azerbaijan", Hostname: "azerbaijan-ca-version-2.expressnetw.com"},
{Country: "Bahamas", Hostname: "bahamas-ca-version-2.expressnetw.com"},
{Country: "Bangladesh", Hostname: "bangladesh-ca-version-2.expressnetw.com"},
{Country: "Belarus", Hostname: "belarus-ca-version-2.expressnetw.com"},
{Country: "Belgium", Hostname: "belgium-ca-version-2.expressnetw.com"},
{Country: "Bermuda", Hostname: "bermuda-ca-version-2.expressnetw.com"},
{Country: "Bhutan", Hostname: "bhutan-ca-version-2.expressnetw.com"},
{Country: "Bolivia", Hostname: "bolivia-ca-version-2.expressnetw.com"},
{Country: "Bosnia and Herzegovina", Hostname: "bosniaandherzegovina-ca-version-2.expressnetw.com"},
{Country: "Brazil", Hostname: "brazil-2-ca-version-2.expressnetw.com"},
{Country: "Brazil", Hostname: "brazil-ca-version-2.expressnetw.com"},
{Country: "Brunei", Hostname: "brunei-ca-version-2.expressnetw.com"},
{Country: "Bulgaria", Hostname: "bulgaria-ca-version-2.expressnetw.com"},
{Country: "Cambodia", Hostname: "cambodia-ca-version-2.expressnetw.com"},
{Country: "Canada", City: "Montreal", Hostname: "canada-montreal-ca-version-2.expressnetw.com"},
{Country: "Canada", City: "Toronto", Hostname: "canada-toronto-2-ca-version-2.expressnetw.com"},
{Country: "Canada", City: "Toronto", Hostname: "canada-toronto-ca-version-2.expressnetw.com"},
{Country: "Cayman Islands", Hostname: "caymanislands-ca-version-2.expressnetw.com"},
{Country: "Chile", Hostname: "chile-ca-version-2.expressnetw.com"},
{Country: "Colombia", Hostname: "colombia-ca-version-2.expressnetw.com"},
{Country: "Costa Rica", Hostname: "costarica-ca-version-2.expressnetw.com"},
{Country: "Croatia", Hostname: "croatia-ca-version-2.expressnetw.com"},
{Country: "Cuba", Hostname: "cuba-ca-version-2.expressnetw.com"},
{Country: "Cyprus", Hostname: "cyprus-ca-version-2.expressnetw.com"},
{Country: "Czech Republic", Hostname: "czechrepublic-ca-version-2.expressnetw.com"},
{Country: "Denmark", Hostname: "denmark-ca-version-2.expressnetw.com"},
{Country: "Dominican Republic", Hostname: "dominicanrepublic-ca-version-2.expressnetw.com"},
{Country: "Ecuador", Hostname: "ecuador-ca-version-2.expressnetw.com"},
{Country: "Egypt", Hostname: "egypt-ca-version-2.expressnetw.com"},
{Country: "Estonia", Hostname: "estonia-ca-version-2.expressnetw.com"},
{Country: "Finland", Hostname: "finland-ca-version-2.expressnetw.com"},
{Country: "France", City: "Alsace", Hostname: "france-alsace-ca-version-2.expressnetw.com"},
{Country: "France", City: "Marseille", Hostname: "france-marseille-ca-version-2.expressnetw.com"},
{Country: "France", City: "Paris", Hostname: "france-paris-1-ca-version-2.expressnetw.com"},
{Country: "France", City: "Paris", Hostname: "france-paris-2-ca-version-2.expressnetw.com"},
{Country: "France", City: "Strasbourg", Hostname: "france-strasbourg-ca-version-2.expressnetw.com"},
{Country: "Georgia", Hostname: "georgia-ca-version-2.expressnetw.com"},
{Country: "Germany", City: "Frankfurt", Hostname: "germany-darmstadt-ca-version-2.expressnetw.com"},
{Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-1-ca-version-2.expressnetw.com"},
{Country: "Germany", City: "Nuremberg", Hostname: "germany-nuremberg-ca-version-2.expressnetw.com"},
{Country: "Ghana", Hostname: "ghana-ca-version-2.expressnetw.com"},
{Country: "Greece", Hostname: "greece-ca-version-2.expressnetw.com"},
{Country: "Guam", Hostname: "guam-ca-version-2.expressnetw.com"},
{Country: "Guatemala", Hostname: "guatemala-ca-version-2.expressnetw.com"},
{Country: "Honduras", Hostname: "honduras-ca-version-2.expressnetw.com"},
{Country: "Hong Kong", Hostname: "hongkong-1-ca-version-2.expressnetw.com"},
{Country: "Hong Kong", Hostname: "hongkong-2-ca-version-2.expressnetw.com"},
{Country: "Hungary", Hostname: "hungary-ca-version-2.expressnetw.com"},
{Country: "Iceland", Hostname: "iceland-ca-version-2.expressnetw.com"},
{Country: "India (via Singapore)", Hostname: "india-sg-ca-version-2.expressnetw.com"},
{Country: "India (via UK)", Hostname: "india-uk-ca-version-2.expressnetw.com"},
{Country: "Indonesia", Hostname: "indonesia-ca-version-2.expressnetw.com"},
{Country: "Ireland", Hostname: "ireland-ca-version-2.expressnetw.com"},
{Country: "Isle of Man", Hostname: "isleofman-ca-version-2.expressnetw.com"},
{Country: "Israel", Hostname: "israel-ca-version-2.expressnetw.com"},
{Country: "Italy", City: "Cosenza", Hostname: "italy-cosenza-ca-version-2.expressnetw.com"},
{Country: "Italy", City: "Milan", Hostname: "italy-milan-ca-version-2.expressnetw.com"},
{Country: "Italy", City: "Naples", Hostname: "italy-naples-ca-version-2.expressnetw.com"},
{Country: "Jamaica", Hostname: "jamaica-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Osaka", Hostname: "japan-osaka-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Shibuya", Hostname: "japan-shibuya-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-ca-version-2.expressnetw.com"},
{Country: "Japan", City: "Yokohama", Hostname: "japan-yokohama-ca-version-2.expressnetw.com"},
{Country: "Jersey", Hostname: "jersey-ca-version-2.expressnetw.com"},
{Country: "Kazakhstan", Hostname: "kazakhstan-ca-version-2.expressnetw.com"},
{Country: "Kenya", Hostname: "kenya-ca-version-2.expressnetw.com"},
{Country: "Laos", Hostname: "laos-ca-version-2.expressnetw.com"},
{Country: "Latvia", Hostname: "latvia-ca-version-2.expressnetw.com"},
{Country: "Lebanon", Hostname: "lebanon-ca-version-2.expressnetw.com"},
{Country: "Liechtenstein", Hostname: "liechtenstein-ca-version-2.expressnetw.com"},
{Country: "Lithuania", Hostname: "lithuania-ca-version-2.expressnetw.com"},
{Country: "Luxembourg", Hostname: "luxembourg-ca-version-2.expressnetw.com"},
{Country: "Macau", Hostname: "macau-ca-version-2.expressnetw.com"},
{Country: "Malaysia", Hostname: "malaysia-ca-version-2.expressnetw.com"},
{Country: "Malta", Hostname: "malta-ca-version-2.expressnetw.com"},
{Country: "Mexico", Hostname: "mexico-ca-version-2.expressnetw.com"},
{Country: "Moldova", Hostname: "moldova-ca-version-2.expressnetw.com"},
{Country: "Monaco", Hostname: "monaco-ca-version-2.expressnetw.com"},
{Country: "Mongolia", Hostname: "mongolia-ca-version-2.expressnetw.com"},
{Country: "Montenegro", Hostname: "montenegro-ca-version-2.expressnetw.com"},
{Country: "Morocco", Hostname: "morocco-ca-version-2.expressnetw.com"},
{Country: "Myanmar", Hostname: "myanmar-ca-version-2.expressnetw.com"},
{Country: "Nepal", Hostname: "nepal-ca-version-2.expressnetw.com"},
{Country: "Netherlands", City: "Amsterdam", Hostname: "netherlands-amsterdam-ca-version-2.expressnetw.com"},
{Country: "Netherlands", City: "Rotterdam", Hostname: "netherlands-rotterdam-ca-version-2.expressnetw.com"},
{Country: "Netherlands", City: "The Hague", Hostname: "netherlands-thehague-ca-version-2.expressnetw.com"},
{Country: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"},
{Country: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"},
{Country: "Norway", Hostname: "norway-ca-version-2.expressnetw.com"},
{Country: "Panama", Hostname: "panama-ca-version-2.expressnetw.com"},
{Country: "Peru", Hostname: "peru-ca-version-2.expressnetw.com"},
{Country: "Philippines (via Singapore)", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"},
{Country: "Poland", Hostname: "poland-ca-version-2.expressnetw.com"},
{Country: "Portugal", Hostname: "portugal-ca-version-2.expressnetw.com"},
{Country: "Puerto Rico", Hostname: "puertorico-ca-version-2.expressnetw.com"},
{Country: "Romania", Hostname: "romania-ca-version-2.expressnetw.com"},
{Country: "Serbia", Hostname: "serbia-ca-version-2.expressnetw.com"},
{Country: "Singapore", City: "CBD", Hostname: "singapore-cbd-ca-version-2.expressnetw.com"},
{Country: "Singapore", City: "Jurong", Hostname: "singapore-jurong-ca-version-2.expressnetw.com"},
{Country: "Singapore", City: "Marina Bay", Hostname: "singapore-marinabay-ca-version-2.expressnetw.com"},
{Country: "Slovakia", Hostname: "slovakia-ca-version-2.expressnetw.com"},
{Country: "Slovenia", Hostname: "slovenia-ca-version-2.expressnetw.com"},
{Country: "South Africa", Hostname: "southafrica-ca-version-2.expressnetw.com"},
{Country: "South Korea", Hostname: "southkorea2-ca-version-2.expressnetw.com"},
{Country: "Spain", City: "Barcelona", Hostname: "spain-barcelona-ca-version-2.expressnetw.com"},
{Country: "Spain", City: "Barcelona", Hostname: "spain-barcelona2-ca-version-2.expressnetw.com"},
{Country: "Spain", City: "Madrid", Hostname: "spain-ca-version-2.expressnetw.com"},
{Country: "Sri Lanka", Hostname: "srilanka-ca-version-2.expressnetw.com"},
{Country: "Sweden", Hostname: "sweden-ca-version-2.expressnetw.com"},
{Country: "Sweden", Hostname: "sweden2-ca-version-2.expressnetw.com"},
{Country: "Switzerland", Hostname: "switzerland-2-ca-version-2.expressnetw.com"},
{Country: "Switzerland", Hostname: "switzerland-ca-version-2.expressnetw.com"},
{Country: "Taiwan", Hostname: "taiwan-3-ca-version-2.expressnetw.com"},
{Country: "Thailand", Hostname: "thailand-ca-version-2.expressnetw.com"},
{Country: "Trinidad and Tobago", Hostname: "trinidadandtobago-ca-version-2.expressnetw.com"},
{Country: "Turkey", Hostname: "turkey-ca-version-2.expressnetw.com"},
{Country: "UK", City: "Docklands", Hostname: "uk-1-docklands-ca-version-2.expressnetw.com"},
{Country: "UK", City: "East London", Hostname: "uk-east-london-ca-version-2.expressnetw.com"},
{Country: "UK", City: "London", Hostname: "uk-london-ca-version-2.expressnetw.com"},
{Country: "UK", City: "Midlands", Hostname: "uk-midlands-ca-version-2.expressnetw.com"},
{Country: "UK", City: "Tottenham", Hostname: "uk-tottenham-ca-version-2.expressnetw.com"},
{Country: "UK", City: "Wembley", Hostname: "uk-wembley-ca-version-2.expressnetw.com"},
{Country: "Ukraine", Hostname: "ukraine-ca-version-2.expressnetw.com"},
{Country: "Uruguay", Hostname: "uruguay-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Albuquerque", Hostname: "usa-albuquerque-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Atlanta", Hostname: "usa-atlanta-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Boston", Hostname: "us-boston-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Chicago", Hostname: "usa-chicago-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Dallas", Hostname: "usa-dallas-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Denver", Hostname: "usa-denver-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Houston", Hostname: "usa-houston-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Jackson", Hostname: "us-jackson-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Lincoln Park", Hostname: "usa-lincolnpark-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Little Rock", Hostname: "us-littlerock-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-2-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-3-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles5-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Miami", Hostname: "usa-miami-2-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Miami", Hostname: "usa-miami-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey-1-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey-3-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey2-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New Orleans", Hostname: "us-neworleans-ca-version-2.expressnetw.com"},
{Country: "USA", City: "New York", Hostname: "usa-newyork-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Oklahoma City", Hostname: "us-oklahoma-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Phoenix", Hostname: "usa-phoenix-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Salt Lake City", Hostname: "usa-saltlakecity-ca-version-2.expressnetw.com"},
{Country: "USA", City: "San Francisco", Hostname: "usa-sanfrancisco-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Santa Monica", Hostname: "usa-santa-monica-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Seattle", Hostname: "usa-seattle-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Tampa", Hostname: "usa-tampa-1-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Washington DC", Hostname: "usa-washingtondc-ca-version-2.expressnetw.com"},
{Country: "USA", City: "Wichita", Hostname: "us-wichita-ca-version-2.expressnetw.com"},
{Country: "Uzbekistan", Hostname: "uzbekistan-ca-version-2.expressnetw.com"},
{Country: "Venezuela", Hostname: "venezuela-ca-version-2.expressnetw.com"},
{Country: "Vietnam", Hostname: "vietnam-ca-version-2.expressnetw.com"},
}
}
@@ -1,25 +0,0 @@
package updater
import (
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
const (
maxFailRatio = 0.4
maxNoNew = 1
maxFails = 4
)
return resolver.ParallelSettings{
Hosts: hosts,
MaxFailRatio: maxFailRatio,
Repeat: resolver.RepeatSettings{
MaxDuration: time.Second,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
}
@@ -1,54 +0,0 @@
package updater
import (
"context"
"fmt"
"sort"
"github.com/qdm12/gluetun/internal/constants/vpn"
"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,
) {
servers = hardcodedServers()
hosts := make([]string, len(servers))
for i := range servers {
hosts[i] = servers[i].Hostname
}
resolveSettings := parallelResolverSettings(hosts)
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
for _, warning := range warnings {
u.warner.Warn(warning)
}
if err != nil {
return nil, err
}
i := 0
for _, server := range servers {
hostname := server.Hostname
server.IPs = hostToIPs[hostname]
if len(server.IPs) == 0 {
continue
}
server.VPN = vpn.OpenVPN
server.UDP = true // no TCP support
servers[i] = server
i++
}
servers = servers[:i]
if len(servers) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
sort.Sort(models.SortableServers(servers))
return servers, nil
}
@@ -1,21 +0,0 @@
package updater
import (
"github.com/qdm12/gluetun/internal/provider/common"
)
type Updater struct {
unzipper common.Unzipper
parallelResolver common.ParallelResolver
warner common.Warner
}
func New(unzipper common.Unzipper, warner common.Warner,
parallelResolver common.ParallelResolver,
) *Updater {
return &Updater{
unzipper: unzipper,
parallelResolver: parallelResolver,
warner: warner,
}
}
+6 -5
View File
@@ -5,23 +5,24 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/fastestvpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/fastestvpn"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
func New(storage common.Storage, client *http.Client, updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
func New(storage common.Storage, client *http.Client, updaterWarner updaters.Warner,
parallelResolver updaters.ParallelResolver,
) *Provider {
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(client, updaterWarner, parallelResolver),
Fetcher: fastestvpn.New(client, updaterWarner, parallelResolver),
}
}
-123
View File
@@ -1,123 +0,0 @@
package updater
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type apiServer struct {
country string
city string
hostname string
}
const apiURL = "https://support.fastestvpn.com/wp-admin/admin-ajax.php"
// The API URL and requests are shamelessly taken from network operations
// done on the page https://support.fastestvpn.com/vpn-servers/
func fetchAPIServers(ctx context.Context, client *http.Client, protocol string) (
servers []apiServer, err error,
) {
form := url.Values{
"action": []string{"vpn_servers"},
"protocol": []string{protocol},
}
body := strings.NewReader(form.Encode())
request, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, body)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// request.Header.Set("User-Agent", "curl/8.9.0")
// request.Header.Set("Accept", "*/*")
response, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("sending request: %w", err)
}
if response.StatusCode != http.StatusOK {
_ = response.Body.Close()
return nil, fmt.Errorf("HTTP status code not OK: %d", response.StatusCode)
}
data, err := io.ReadAll(response.Body)
if err != nil {
_ = response.Body.Close()
return nil, fmt.Errorf("reading response body: %w", err)
}
err = response.Body.Close()
if err != nil {
return nil, fmt.Errorf("closing response body: %w", err)
}
const usualMaxNumber = 100
servers = make([]apiServer, 0, usualMaxNumber)
for {
trBlock := getNextTRBlock(data)
if trBlock == nil {
break
}
data = data[len(trBlock):]
var server apiServer
const numberOfTDBlocks = 3
for i := range numberOfTDBlocks {
tdBlock := getNextTDBlock(trBlock)
if tdBlock == nil {
return nil, fmt.Errorf("data is malformed: expected 3 <td> blocks in <tr> block %q",
string(trBlock))
}
trBlock = trBlock[len(tdBlock):]
const startToken, endToken = "<td>", "</td>"
tdBlockData := string(tdBlock[len(startToken) : len(tdBlock)-len(endToken)])
const countryIndex, cityIndex, hostnameIndex = 0, 1, 2
switch i {
case countryIndex:
server.country = tdBlockData
case cityIndex:
server.city = tdBlockData
case hostnameIndex:
server.hostname = tdBlockData
}
}
servers = append(servers, server)
}
return servers, nil
}
func getNextTRBlock(data []byte) (trBlock []byte) {
const startToken, endToken = "<tr>", "</tr>"
return getNextBlock(data, startToken, endToken)
}
func getNextTDBlock(data []byte) (tdBlock []byte) {
const startToken, endToken = "<td>", "</td>"
return getNextBlock(data, startToken, endToken)
}
func getNextBlock(data []byte, startToken, endToken string) (nextBlock []byte) {
i := bytes.Index(data, []byte(startToken))
if i == -1 {
return nil
}
nextBlock = data[i:]
i = bytes.Index(nextBlock[len(startToken):], []byte(endToken))
if i == -1 {
return nil
}
nextBlock = nextBlock[:i+len(startToken)+len(endToken)]
return nextBlock
}
@@ -1,157 +0,0 @@
package updater
import (
"context"
"errors"
"io"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
type roundTripFunc func(r *http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return f(r)
}
func Test_fechAPIServers(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
ctx context.Context
protocol string
requestBody string
responseStatus int
responseBody io.ReadCloser
transportErr error
servers []apiServer
errMessage string
}{
"transport_error": {
ctx: context.Background(),
protocol: "tcp",
requestBody: "action=vpn_servers&protocol=tcp",
responseStatus: http.StatusOK,
transportErr: errors.New("test error"),
errMessage: `sending request: Post ` +
`"https://support.fastestvpn.com/wp-admin/admin-ajax.php": ` +
`test error`,
},
"not_found_status_code": {
ctx: context.Background(),
protocol: "tcp",
requestBody: "action=vpn_servers&protocol=tcp",
responseStatus: http.StatusNotFound,
errMessage: "HTTP status code not OK: 404",
},
"empty_data": {
ctx: context.Background(),
protocol: "tcp",
requestBody: "action=vpn_servers&protocol=tcp",
responseStatus: http.StatusOK,
responseBody: io.NopCloser(strings.NewReader("")),
servers: []apiServer{},
},
"single_server": {
ctx: context.Background(),
protocol: "tcp",
requestBody: "action=vpn_servers&protocol=tcp",
responseStatus: http.StatusOK,
responseBody: io.NopCloser(strings.NewReader(
"irrelevant<tr><td>Australia</td><td>Sydney</td>" +
"<td>au-stream.jumptoserver.com</td></tr>irrelevant")),
servers: []apiServer{
{country: "Australia", city: "Sydney", hostname: "au-stream.jumptoserver.com"},
},
},
"two_servers": {
ctx: context.Background(),
protocol: "tcp",
requestBody: "action=vpn_servers&protocol=tcp",
responseStatus: http.StatusOK,
responseBody: io.NopCloser(strings.NewReader(
"<tr><td>Australia</td><td>Sydney</td><td>au-stream.jumptoserver.com</td></tr>" +
"<tr><td>Australia</td><td>Sydney</td><td>au-01.jumptoserver.com</td></tr>")),
servers: []apiServer{
{country: "Australia", city: "Sydney", hostname: "au-stream.jumptoserver.com"},
{country: "Australia", city: "Sydney", hostname: "au-01.jumptoserver.com"},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, apiURL, r.URL.String())
requestBody, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Equal(t, testCase.requestBody, string(requestBody))
if testCase.transportErr != nil {
return nil, testCase.transportErr
}
return &http.Response{
StatusCode: testCase.responseStatus,
Body: testCase.responseBody,
}, nil
}),
}
entries, err := fetchAPIServers(testCase.ctx, client, testCase.protocol)
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.servers, entries)
})
}
}
func Test_getNextBlock(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
data string
startToken string
endToken string
nextBlock []byte
}{
"empty_data": {
startToken: "<a>",
endToken: "</a>",
},
"start_token_not_found": {
data: "test</a>",
startToken: "<a>",
endToken: "</a>",
},
"end_token_not_found": {
data: "<a>test",
startToken: "<a>",
endToken: "</a>",
},
"block_found": {
data: "xy<a>test</a><a>test2</a>zx",
startToken: "<a>",
endToken: "</a>",
nextBlock: []byte("<a>test</a>"),
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
nextBlock := getNextBlock([]byte(testCase.data), testCase.startToken, testCase.endToken)
assert.Equal(t, testCase.nextBlock, nextBlock)
})
}
}
@@ -1,93 +0,0 @@
package updater
import (
"net/netip"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
)
type hostToServerData map[string]serverData
type serverData struct {
openvpn bool
wireguard bool
country string
city string
openvpnUDP bool
openvpnTCP bool
ips []netip.Addr
}
func (hts hostToServerData) add(host, vpnType, country, city string, tcp, udp bool) {
serverData, ok := hts[host]
switch vpnType {
case vpn.OpenVPN:
serverData.openvpn = true
serverData.openvpnTCP = serverData.openvpnTCP || tcp
serverData.openvpnUDP = serverData.openvpnUDP || udp
case vpn.Wireguard:
serverData.wireguard = true
default:
panic("protocol not supported")
}
if !ok {
serverData.country = country
serverData.city = city
} else if city != "" {
// some servers are listed without the city although
// they are also listed with the city described, so update
// the city field.
serverData.city = city
}
hts[host] = serverData
}
func (hts hostToServerData) toHostsSlice() (hosts []string) {
hosts = make([]string, 0, len(hts))
for host := range hts {
hosts = append(hosts, host)
}
return hosts
}
func (hts hostToServerData) adaptWithIPs(hostToIPs map[string][]netip.Addr) {
for host, serverData := range hts {
ips := hostToIPs[host]
if len(ips) == 0 {
delete(hts, host)
continue
}
serverData.ips = ips
hts[host] = serverData
}
}
func (hts hostToServerData) toServersSlice() (servers []models.Server) {
servers = make([]models.Server, 0, 2*len(hts)) //nolint:mnd
for hostname, serverData := range hts {
baseServer := models.Server{
Hostname: hostname,
Country: serverData.country,
City: serverData.city,
IPs: serverData.ips,
}
if serverData.openvpn {
openvpnServer := baseServer
openvpnServer.VPN = vpn.OpenVPN
openvpnServer.TCP = serverData.openvpnTCP
openvpnServer.UDP = serverData.openvpnUDP
servers = append(servers, openvpnServer)
}
if serverData.wireguard {
wireguardServer := baseServer
wireguardServer.VPN = vpn.Wireguard
const wireguardPublicKey = "658QxufMbjOTmB61Z7f+c7Rjg7oqWLnepTalqBERjF0="
wireguardServer.WgPubKey = wireguardPublicKey
servers = append(servers, wireguardServer)
}
}
return servers
}
@@ -1,26 +0,0 @@
package updater
import (
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
const (
maxFailRatio = 0.1
maxNoNew = 1
maxFails = 4
maxDuration = 3 * time.Second
)
return resolver.ParallelSettings{
Hosts: hosts,
MaxFailRatio: maxFailRatio,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
}
@@ -1,61 +0,0 @@
package updater
import (
"context"
"fmt"
"sort"
"github.com/qdm12/gluetun/internal/constants/vpn"
"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,
) {
protocols := []string{"ikev2", "tcp", "udp"}
hts := make(hostToServerData)
for _, protocol := range protocols {
apiServers, err := fetchAPIServers(ctx, u.client, protocol)
if err != nil {
return nil, fmt.Errorf("fetching %s servers from API: %w", protocol, err)
}
for _, apiServer := range apiServers {
// all hostnames from the protocols TCP, UDP and IKEV2 support Wireguard
// per https://github.com/qdm12/gluetun-wiki/issues/76#issuecomment-2125420536
const wgTCP, wgUDP = false, false // ignored
hts.add(apiServer.hostname, vpn.Wireguard, apiServer.country, apiServer.city, wgTCP, wgUDP)
tcp := protocol == "tcp"
udp := protocol == "udp"
if !tcp && !udp { // not an OpenVPN protocol, for example ikev2
continue
}
hts.add(apiServer.hostname, vpn.OpenVPN, apiServer.country, apiServer.city, tcp, udp)
}
}
hosts := hts.toHostsSlice()
resolveSettings := parallelResolverSettings(hosts)
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
for _, warning := range warnings {
u.warner.Warn(warning)
}
if err != nil {
return nil, err
}
hts.adaptWithIPs(hostToIPs)
servers = hts.toServersSlice()
if len(servers) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
sort.Sort(models.SortableServers(servers))
return servers, nil
}
@@ -1,23 +0,0 @@
package updater
import (
"net/http"
"github.com/qdm12/gluetun/internal/provider/common"
)
type Updater struct {
client *http.Client
parallelResolver common.ParallelResolver
warner common.Warner
}
func New(client *http.Client, warner common.Warner,
parallelResolver common.ParallelResolver,
) *Updater {
return &Updater{
client: client,
parallelResolver: parallelResolver,
warner: warner,
}
}
+6 -5
View File
@@ -3,23 +3,24 @@ package giganews
import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/giganews/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/giganews"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
func New(storage common.Storage, unzipper updaters.Unzipper, updaterWarner updaters.Warner,
parallelResolver updaters.ParallelResolver,
) *Provider {
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
Fetcher: giganews.New(unzipper, updaterWarner, parallelResolver),
}
}
@@ -1,19 +0,0 @@
package updater
import (
"fmt"
"strings"
)
func parseFilename(fileName string) (
region string, err error,
) {
const suffix = ".ovpn"
if !strings.HasSuffix(fileName, suffix) {
return "", fmt.Errorf("filename does not have the openvpn file extension: %s", fileName)
}
region = strings.TrimSuffix(fileName, suffix)
region = strings.ReplaceAll(region, " - ", " ")
return region, nil
}
@@ -1,55 +0,0 @@
package updater
import (
"net/netip"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
)
type hostToServer map[string]models.Server
func (hts hostToServer) add(host, region string, tcp, udp bool) {
server, ok := hts[host]
if !ok {
server.VPN = vpn.OpenVPN
server.Hostname = host
server.Region = region
}
if tcp {
server.TCP = true
}
if udp {
server.UDP = true
}
hts[host] = server
}
func (hts hostToServer) toHostsSlice() (hosts []string) {
hosts = make([]string, 0, len(hts))
for host := range hts {
hosts = append(hosts, host)
}
return hosts
}
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]netip.Addr) {
for host, IPs := range hostToIPs {
server := hts[host]
server.IPs = IPs
hts[host] = server
}
for host, server := range hts {
if len(server.IPs) == 0 {
delete(hts, host)
}
}
}
func (hts hostToServer) toServersSlice() (servers []models.Server) {
servers = make([]models.Server, 0, len(hts))
for _, server := range hts {
servers = append(servers, server)
}
return servers
}
@@ -1,28 +0,0 @@
package updater
import (
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
const (
maxFailRatio = 0.1
maxDuration = 5 * time.Second
betweenDuration = time.Second
maxNoNew = 2
maxFails = 2
)
return resolver.ParallelSettings{
Hosts: hosts,
MaxFailRatio: maxFailRatio,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
BetweenDuration: betweenDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
}
@@ -1,88 +0,0 @@
package updater
import (
"context"
"fmt"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/updater/openvpn"
)
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
servers []models.Server, err error,
) {
const url = "https://support.vyprvpn.com/hc/article_attachments/360052617332/Vypr_OpenVPN_20200320.zip"
contents, err := u.unzipper.FetchAndExtract(ctx, url)
if err != nil {
return nil, err
} else if len(contents) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(contents), minServers)
}
hts := make(hostToServer)
for fileName, content := range contents {
if !strings.HasSuffix(fileName, ".ovpn") {
continue // not an OpenVPN file
}
host, warning, err := openvpn.ExtractHost(content)
if warning != "" {
u.warner.Warn(warning)
}
if err != nil {
// treat error as warning and go to next file
u.warner.Warn(err.Error() + " in " + fileName)
continue
}
host = strings.ReplaceAll(host, "vyprvpn.com", "vpn.giganews.com")
tcp, udp, err := openvpn.ExtractProto(content)
if err != nil {
// treat error as warning and go to next file
u.warner.Warn(err.Error() + " in " + fileName)
continue
}
region, err := parseFilename(fileName)
if err != nil {
// treat error as warning and go to next file
u.warner.Warn(err.Error())
continue
}
hts.add(host, region, tcp, udp)
}
if len(hts) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(hts), minServers)
}
hosts := hts.toHostsSlice()
resolveSettings := parallelResolverSettings(hosts)
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
for _, warning := range warnings {
u.warner.Warn(warning)
}
if err != nil {
return nil, err
}
if len(hostToIPs) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
hts.adaptWithIPs(hostToIPs)
servers = hts.toServersSlice()
sort.Sort(models.SortableServers(servers))
return servers, nil
}
@@ -1,21 +0,0 @@
package updater
import (
"github.com/qdm12/gluetun/internal/provider/common"
)
type Updater struct {
unzipper common.Unzipper
parallelResolver common.ParallelResolver
warner common.Warner
}
func New(unzipper common.Unzipper, warner common.Warner,
parallelResolver common.ParallelResolver,
) *Updater {
return &Updater{
unzipper: unzipper,
parallelResolver: parallelResolver,
warner: warner,
}
}
+6 -5
View File
@@ -5,23 +5,24 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/hidemyass/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/hidemyass"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
func New(storage common.Storage, client *http.Client, updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
func New(storage common.Storage, client *http.Client, updaterWarner updaters.Warner,
parallelResolver updaters.ParallelResolver,
) *Provider {
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(client, updaterWarner, parallelResolver),
Fetcher: hidemyass.New(client, updaterWarner, parallelResolver),
}
}
@@ -1,20 +0,0 @@
package updater
func getUniqueHosts(tcpHostToURL, udpHostToURL map[string]string) (
hosts []string,
) {
uniqueHosts := make(map[string]struct{}, len(tcpHostToURL))
for host := range tcpHostToURL {
uniqueHosts[host] = struct{}{}
}
for host := range udpHostToURL {
uniqueHosts[host] = struct{}{}
}
hosts = make([]string, 0, len(uniqueHosts))
for host := range uniqueHosts {
hosts = append(hosts, host)
}
return hosts
}
@@ -1,45 +0,0 @@
package updater
import (
"context"
"net/http"
"strings"
"github.com/qdm12/gluetun/internal/updater/openvpn"
)
func getAllHostToURL(ctx context.Context, client *http.Client) (
tcpHostToURL, udpHostToURL map[string]string, err error,
) {
tcpHostToURL, err = getHostToURL(ctx, client, "TCP")
if err != nil {
return nil, nil, err
}
udpHostToURL, err = getHostToURL(ctx, client, "UDP")
if err != nil {
return nil, nil, err
}
return tcpHostToURL, udpHostToURL, nil
}
func getHostToURL(ctx context.Context, client *http.Client, protocol string) (
hostToURL map[string]string, err error,
) {
const baseURL = "https://vpn.hidemyass.com/vpn-config"
indexURL := baseURL + "/" + strings.ToUpper(protocol) + "/"
urls, err := fetchIndex(ctx, client, indexURL)
if err != nil {
return nil, err
}
const failEarly = true
hostToURL, errors := openvpn.FetchMultiFiles(ctx, client, urls, failEarly)
if len(errors) > 0 {
return nil, errors[0]
}
return hostToURL, nil
}
@@ -1,55 +0,0 @@
package updater
import (
"context"
"io"
"net/http"
"regexp"
"strings"
)
var indexOpenvpnLinksRegex = regexp.MustCompile(`<a[ ]+href=".+\.ovpn">.+\.ovpn</a>`)
func fetchIndex(ctx context.Context, client *http.Client, indexURL string) (
openvpnURLs []string, err error,
) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, indexURL, nil)
if err != nil {
return nil, err
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
htmlCode, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
if !strings.HasSuffix(indexURL, "/") {
indexURL += "/"
}
lines := strings.Split(string(htmlCode), "\n")
for _, line := range lines {
found := indexOpenvpnLinksRegex.FindString(line)
if len(found) == 0 {
continue
}
const prefix = `.ovpn">`
const suffix = `</a>`
startIndex := strings.Index(found, prefix) + len(prefix)
endIndex := strings.Index(found, suffix)
filename := found[startIndex:endIndex]
openvpnURL := indexURL + filename
if !strings.HasSuffix(openvpnURL, ".ovpn") {
continue
}
openvpnURLs = append(openvpnURLs, openvpnURL)
}
return openvpnURLs, nil
}
@@ -1,28 +0,0 @@
package updater
import (
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
const (
maxFailRatio = 0.1
maxDuration = 15 * time.Second
betweenDuration = 2 * time.Second
maxNoNew = 2
maxFails = 2
)
return resolver.ParallelSettings{
Hosts: hosts,
MaxFailRatio: maxFailRatio,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
BetweenDuration: betweenDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
}
@@ -1,74 +0,0 @@
package updater
import (
"context"
"fmt"
"sort"
"github.com/qdm12/gluetun/internal/constants/vpn"
"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,
) {
tcpHostToURL, udpHostToURL, err := getAllHostToURL(ctx, u.client)
if err != nil {
return nil, err
}
hosts := getUniqueHosts(tcpHostToURL, udpHostToURL)
if len(hosts) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(hosts), minServers)
}
resolveSettings := parallelResolverSettings(hosts)
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
for _, warning := range warnings {
u.warner.Warn(warning)
}
if err != nil {
return nil, err
}
if len(hostToIPs) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
servers = make([]models.Server, 0, len(hostToIPs))
for host, IPs := range hostToIPs {
tcpURL, tcp := tcpHostToURL[host]
udpURL, udp := udpHostToURL[host]
// These two are only used to extract the country, region and city.
var url, protocol string
if tcp {
url = tcpURL
protocol = "TCP"
} else if udp {
url = udpURL
protocol = "UDP"
}
country, region, city := parseOpenvpnURL(url, protocol)
server := models.Server{
VPN: vpn.OpenVPN,
Country: country,
Region: region,
City: city,
Hostname: host,
IPs: IPs,
TCP: tcp,
UDP: udp,
}
servers = append(servers, server)
}
sort.Sort(models.SortableServers(servers))
return servers, nil
}
@@ -1,23 +0,0 @@
package updater
import (
"net/http"
"github.com/qdm12/gluetun/internal/provider/common"
)
type Updater struct {
client *http.Client
parallelResolver common.ParallelResolver
warner common.Warner
}
func New(client *http.Client, warner common.Warner,
parallelResolver common.ParallelResolver,
) *Updater {
return &Updater{
client: client,
parallelResolver: parallelResolver,
warner: warner,
}
}
@@ -1,58 +0,0 @@
package updater
import (
"strings"
"unicode"
)
func parseOpenvpnURL(url, protocol string) (country, region, city string) {
lastSlashIndex := strings.LastIndex(url, "/")
url = url[lastSlashIndex+1:]
suffix := "." + strings.ToUpper(protocol) + ".ovpn"
url = strings.TrimSuffix(url, suffix)
parts := strings.Split(url, ".")
switch len(parts) {
case 1:
country = parts[0]
return country, "", ""
case 2: //nolint:mnd
country = parts[0]
city = parts[1]
default:
country = parts[0]
region = parts[1]
city = parts[2]
}
country = camelCaseToWords(country)
region = camelCaseToWords(region)
city = camelCaseToWords(city)
country = mutateSpecialCountryCases(country)
return country, region, city
}
func camelCaseToWords(camelCase string) (words string) {
wasLowerCase := false
for _, r := range camelCase {
if wasLowerCase && unicode.IsUpper(r) {
words += " "
}
wasLowerCase = unicode.IsLower(r)
words += string(r)
}
return words
}
func mutateSpecialCountryCases(country string) string {
switch country {
case "Coted`Ivoire":
return "Cote d'Ivoire"
default:
return country
}
}
+6 -5
View File
@@ -3,23 +3,24 @@ package ipvanish
import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/ipvanish/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/ipvanish"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
func New(storage common.Storage, unzipper common.Unzipper, updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
func New(storage common.Storage, unzipper updaters.Unzipper, updaterWarner updaters.Warner,
parallelResolver updaters.ParallelResolver,
) *Provider {
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
Fetcher: ipvanish.New(unzipper, updaterWarner, parallelResolver),
}
}
@@ -1,38 +0,0 @@
package updater
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"golang.org/x/text/cases"
)
func parseFilename(fileName, hostname string, titleCaser cases.Caser) (
country, city string, err error,
) {
const prefix = "ipvanish-"
s := strings.TrimPrefix(fileName, prefix)
const ext = ".ovpn"
host := strings.Split(hostname, ".")[0]
suffix := "-" + host + ext
s = strings.TrimSuffix(s, suffix)
parts := strings.Split(s, "-")
countryCodes := constants.CountryCodes()
countryCode := strings.ToLower(parts[0])
country, ok := countryCodes[countryCode]
if !ok {
return "", "", fmt.Errorf("country code is unknown: %s", countryCode)
}
country = titleCaser.String(country)
if len(parts) > 1 {
city = strings.Join(parts[1:], " ")
city = titleCaser.String(city)
}
return country, city, nil
}
@@ -1,56 +0,0 @@
package updater
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func Test_parseFilename(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
fileName string
hostname string
country string
city string
err error
}{
"unknown country code": {
fileName: "ipvanish-unknown-host.ovpn",
hostname: "host.ipvanish.com",
err: errors.New("country code is unknown: unknown"),
},
"country code only": {
fileName: "ipvanish-ca-host.ovpn",
hostname: "host.ipvanish.com",
country: "Canada",
},
"country code and city": {
fileName: "ipvanish-ca-sao-paulo-host.ovpn",
hostname: "host.ipvanish.com",
country: "Canada",
city: "Sao Paulo",
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
titleCaser := cases.Title(language.English)
country, city, err := parseFilename(testCase.fileName, testCase.hostname, titleCaser)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.country, country)
assert.Equal(t, testCase.city, city)
})
}
}
@@ -1,60 +0,0 @@
package updater
import (
"net/netip"
"sort"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
)
type hostToServer map[string]models.Server
func (hts hostToServer) add(host, country, city string, tcp, udp bool) {
server, ok := hts[host]
if !ok {
server.VPN = vpn.OpenVPN
server.Hostname = host
server.Country = country
server.City = city
}
if tcp {
server.TCP = tcp
}
if udp {
server.UDP = udp
}
hts[host] = server
}
func (hts hostToServer) toHostsSlice() (hosts []string) {
hosts = make([]string, 0, len(hts))
for host := range hts {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool {
return hosts[i] < hosts[j]
})
return hosts
}
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]netip.Addr) {
for host, IPs := range hostToIPs {
server := hts[host]
server.IPs = IPs
hts[host] = server
}
for host, server := range hts {
if len(server.IPs) == 0 {
delete(hts, host)
}
}
}
func (hts hostToServer) toServersSlice() (servers []models.Server) {
servers = make([]models.Server, 0, len(hts))
for _, server := range hts {
servers = append(servers, server)
}
return servers
}
@@ -1,212 +0,0 @@
package updater
import (
"net/netip"
"testing"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
)
func Test_hostToServer_add(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
initialHTS hostToServer
host string
country string
city string
tcp bool
udp bool
expectedHTS hostToServer
}{
"empty host to server": {
initialHTS: hostToServer{},
host: "host",
country: "country",
city: "city",
tcp: true,
udp: true,
expectedHTS: hostToServer{
"host": {
VPN: vpn.OpenVPN,
Hostname: "host",
Country: "country",
City: "city",
TCP: true,
UDP: true,
},
},
},
"add server": {
initialHTS: hostToServer{
"existing host": {},
},
host: "host",
country: "country",
city: "city",
tcp: true,
udp: true,
expectedHTS: hostToServer{
"existing host": {},
"host": models.Server{
VPN: vpn.OpenVPN,
Hostname: "host",
Country: "country",
City: "city",
TCP: true,
UDP: true,
},
},
},
"extend existing server": {
initialHTS: hostToServer{
"host": models.Server{
VPN: vpn.OpenVPN,
Hostname: "host",
Country: "country",
City: "city",
TCP: true,
},
},
host: "host",
country: "country",
city: "city",
tcp: false,
udp: true,
expectedHTS: hostToServer{
"host": models.Server{
VPN: vpn.OpenVPN,
Hostname: "host",
Country: "country",
City: "city",
TCP: true,
UDP: true,
},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
testCase.initialHTS.add(testCase.host, testCase.country, testCase.city, testCase.tcp, testCase.udp)
assert.Equal(t, testCase.expectedHTS, testCase.initialHTS)
})
}
}
func Test_hostToServer_toHostsSlice(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
hts hostToServer
hosts []string
}{
"empty host to server": {
hts: hostToServer{},
hosts: []string{},
},
"single host": {
hts: hostToServer{
"A": {},
},
hosts: []string{"A"},
},
"multiple hosts": {
hts: hostToServer{
"A": {},
"B": {},
},
hosts: []string{"A", "B"},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
hosts := testCase.hts.toHostsSlice()
assert.ElementsMatch(t, testCase.hosts, hosts)
})
}
}
func Test_hostToServer_adaptWithIPs(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
initialHTS hostToServer
hostToIPs map[string][]netip.Addr
expectedHTS hostToServer
}{
"create server": {
initialHTS: hostToServer{},
hostToIPs: map[string][]netip.Addr{
"A": {netip.AddrFrom4([4]byte{1, 2, 3, 4})},
},
expectedHTS: hostToServer{
"A": models.Server{
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
},
},
},
"add IPs to existing server": {
initialHTS: hostToServer{
"A": models.Server{
Country: "country",
},
},
hostToIPs: map[string][]netip.Addr{
"A": {netip.AddrFrom4([4]byte{1, 2, 3, 4})},
},
expectedHTS: hostToServer{
"A": models.Server{
Country: "country",
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
},
},
},
"remove server without IP": {
initialHTS: hostToServer{
"A": models.Server{
Country: "country",
},
},
hostToIPs: map[string][]netip.Addr{},
expectedHTS: hostToServer{},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
testCase.initialHTS.adaptWithIPs(testCase.hostToIPs)
assert.Equal(t, testCase.expectedHTS, testCase.initialHTS)
})
}
}
func Test_hostToServer_toServersSlice(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
hts hostToServer
servers []models.Server
}{
"empty host to server": {
hts: hostToServer{},
servers: []models.Server{},
},
"multiple servers": {
hts: hostToServer{
"A": {Country: "A"},
"B": {Country: "B"},
},
servers: []models.Server{
{Country: "A"},
{Country: "B"},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
servers := testCase.hts.toServersSlice()
assert.ElementsMatch(t, testCase.servers, servers)
})
}
}
@@ -1,28 +0,0 @@
package updater
import (
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
const (
maxFailRatio = 0.1
maxDuration = 20 * time.Second
betweenDuration = time.Second
maxNoNew = 2
maxFails = 2
)
return resolver.ParallelSettings{
Hosts: hosts,
MaxFailRatio: maxFailRatio,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
BetweenDuration: betweenDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
}
@@ -1,90 +0,0 @@
package updater
import (
"context"
"fmt"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/updater/openvpn"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
servers []models.Server, err error,
) {
const url = "https://configs.ipvanish.com/openvpn/v2.6.0-0/configs.zip"
contents, err := u.unzipper.FetchAndExtract(ctx, url)
if err != nil {
return nil, err
} else if len(contents) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(contents), minServers)
}
hts := make(hostToServer)
titleCaser := cases.Title(language.English)
for fileName, content := range contents {
if !strings.HasSuffix(fileName, ".ovpn") {
continue // not an OpenVPN file
}
tcp, udp, err := openvpn.ExtractProto(content)
if err != nil {
// treat error as warning and go to next file
u.warner.Warn(err.Error() + " in " + fileName)
continue
}
hostname, warning, err := openvpn.ExtractHost(content)
if warning != "" {
u.warner.Warn(warning)
}
if err != nil {
// treat error as warning and go to next file
u.warner.Warn(err.Error() + " in " + fileName)
continue
}
country, city, err := parseFilename(fileName, hostname, titleCaser)
if err != nil {
// treat error as warning and go to next file
u.warner.Warn(err.Error() + " in " + fileName)
continue
}
hts.add(hostname, country, city, tcp, udp)
}
if len(hts) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(hts), minServers)
}
hosts := hts.toHostsSlice()
resolveSettings := parallelResolverSettings(hosts)
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
for _, warning := range warnings {
u.warner.Warn(warning)
}
if err != nil {
return nil, err
}
if len(hostToIPs) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
hts.adaptWithIPs(hostToIPs)
servers = hts.toServersSlice()
sort.Sort(models.SortableServers(servers))
return servers, nil
}
@@ -1,224 +0,0 @@
package updater
import (
"context"
"errors"
"net/netip"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Updater_GetServers(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
// Inputs
minServers int
// Mocks
warnerBuilder func(ctrl *gomock.Controller) common.Warner
// Unzip
unzipContents map[string][]byte
unzipErr error
// Resolution
expectResolve bool
resolverSettings resolver.ParallelSettings
hostToIPs map[string][]netip.Addr
resolveWarnings []string
resolveErr error
// Output
servers []models.Server
err error
}{
"unzipper error": {
warnerBuilder: func(_ *gomock.Controller) common.Warner { return nil },
unzipErr: errors.New("dummy"),
err: errors.New("dummy"),
},
"not enough unzip contents": {
minServers: 1,
warnerBuilder: func(_ *gomock.Controller) common.Warner { return nil },
unzipContents: map[string][]byte{},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"no openvpn file": {
minServers: 1,
warnerBuilder: func(_ *gomock.Controller) common.Warner { return nil },
unzipContents: map[string][]byte{"somefile.txt": {}},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"invalid proto": {
minServers: 1,
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
warner := common.NewMockWarner(ctrl)
warner.EXPECT().Warn("unknown protocol: invalid in badproto.ovpn")
return warner
},
unzipContents: map[string][]byte{"badproto.ovpn": []byte(`proto invalid`)},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"no host": {
minServers: 1,
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
warner := common.NewMockWarner(ctrl)
warner.EXPECT().Warn("remote host not found in nohost.ovpn")
return warner
},
unzipContents: map[string][]byte{"nohost.ovpn": []byte(``)},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"multiple hosts": {
minServers: 1,
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
warner := common.NewMockWarner(ctrl)
warner.EXPECT().Warn("only using the first host \"hosta\" and discarding 1 other hosts")
return warner
},
unzipContents: map[string][]byte{
"ipvanish-CA-City-A-hosta.ovpn": []byte("remote hosta\nremote hostb"),
},
expectResolve: true,
resolverSettings: resolver.ParallelSettings{
Hosts: []string{"hosta"},
MaxFailRatio: 0.1,
Repeat: resolver.RepeatSettings{
MaxDuration: 20 * time.Second,
BetweenDuration: time.Second,
MaxNoNew: 2,
MaxFails: 2,
SortIPs: true,
},
},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"resolve error": {
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
warner := common.NewMockWarner(ctrl)
warner.EXPECT().Warn("resolve warning")
return warner
},
unzipContents: map[string][]byte{
"ipvanish-CA-City-A-hosta.ovpn": []byte("remote hosta"),
},
expectResolve: true,
resolverSettings: resolver.ParallelSettings{
Hosts: []string{"hosta"},
MaxFailRatio: 0.1,
Repeat: resolver.RepeatSettings{
MaxDuration: 20 * time.Second,
BetweenDuration: time.Second,
MaxNoNew: 2,
MaxFails: 2,
SortIPs: true,
},
},
resolveWarnings: []string{"resolve warning"},
resolveErr: errors.New("dummy"),
err: errors.New("dummy"),
},
"filename parsing error": {
minServers: 1,
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
warner := common.NewMockWarner(ctrl)
warner.EXPECT().Warn("country code is unknown: unknown in ipvanish-unknown-City-A-hosta.ovpn")
return warner
},
unzipContents: map[string][]byte{
"ipvanish-unknown-City-A-hosta.ovpn": []byte("remote hosta"),
},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"success": {
minServers: 1,
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
warner := common.NewMockWarner(ctrl)
warner.EXPECT().Warn("resolve warning")
return warner
},
unzipContents: map[string][]byte{
"ipvanish-CA-City-A-hosta.ovpn": []byte("remote hosta"),
"ipvanish-LU-City-B-hostb.ovpn": []byte("remote hostb"),
},
expectResolve: true,
resolverSettings: resolver.ParallelSettings{
Hosts: []string{"hosta", "hostb"},
MaxFailRatio: 0.1,
Repeat: resolver.RepeatSettings{
MaxDuration: 20 * time.Second,
BetweenDuration: time.Second,
MaxNoNew: 2,
MaxFails: 2,
SortIPs: true,
},
},
hostToIPs: map[string][]netip.Addr{
"hosta": {netip.AddrFrom4([4]byte{1, 1, 1, 1}), netip.AddrFrom4([4]byte{2, 2, 2, 2})},
"hostb": {netip.AddrFrom4([4]byte{3, 3, 3, 3}), netip.AddrFrom4([4]byte{4, 4, 4, 4})},
},
resolveWarnings: []string{"resolve warning"},
servers: []models.Server{
{
VPN: vpn.OpenVPN,
Country: "Canada",
City: "City A",
Hostname: "hosta",
UDP: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1}), netip.AddrFrom4([4]byte{2, 2, 2, 2})},
},
{
VPN: vpn.OpenVPN,
Country: "Luxembourg",
City: "City B",
Hostname: "hostb",
UDP: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{3, 3, 3, 3}), netip.AddrFrom4([4]byte{4, 4, 4, 4})},
},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := context.Background()
unzipper := common.NewMockUnzipper(ctrl)
const zipURL = "https://configs.ipvanish.com/openvpn/v2.6.0-0/configs.zip"
unzipper.EXPECT().FetchAndExtract(ctx, zipURL).
Return(testCase.unzipContents, testCase.unzipErr)
parallelResolver := common.NewMockParallelResolver(ctrl)
if testCase.expectResolve {
parallelResolver.EXPECT().Resolve(ctx, testCase.resolverSettings).
Return(testCase.hostToIPs, testCase.resolveWarnings, testCase.resolveErr)
}
updater := &Updater{
unzipper: unzipper,
warner: testCase.warnerBuilder(ctrl),
parallelResolver: parallelResolver,
}
servers, err := updater.FetchServers(ctx, testCase.minServers)
assert.Equal(t, testCase.servers, servers)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
@@ -1,21 +0,0 @@
package updater
import (
"github.com/qdm12/gluetun/internal/provider/common"
)
type Updater struct {
unzipper common.Unzipper
warner common.Warner
parallelResolver common.ParallelResolver
}
func New(unzipper common.Unzipper, warner common.Warner,
parallelResolver common.ParallelResolver,
) *Updater {
return &Updater{
unzipper: unzipper,
warner: warner,
parallelResolver: parallelResolver,
}
}
+13 -12
View File
@@ -8,11 +8,12 @@ import (
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/constants"
umodels "github.com/qdm12/gluetun/pkg/updaters/models"
"github.com/stretchr/testify/assert"
)
@@ -22,7 +23,7 @@ func Test_Provider_GetConnection(t *testing.T) {
const provider = providers.Ivpn
testCases := map[string]struct {
filteredServers []models.Server
filteredServers []umodels.Server
storageErr error
selection settings.ServerSelection
ipv6Supported bool
@@ -34,7 +35,7 @@ func Test_Provider_GetConnection(t *testing.T) {
errMessage: "filtering servers: test error",
},
"default OpenVPN TCP port": {
filteredServers: []models.Server{
filteredServers: []umodels.Server{
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}},
},
selection: settings.ServerSelection{
@@ -43,14 +44,14 @@ func Test_Provider_GetConnection(t *testing.T) {
},
}.WithDefaults(provider),
connection: models.Connection{
Type: vpn.OpenVPN,
Type: constants.OpenVPN,
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
Port: 443,
Protocol: constants.TCP,
},
},
"default OpenVPN UDP port": {
filteredServers: []models.Server{
filteredServers: []umodels.Server{
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}},
},
selection: settings.ServerSelection{
@@ -59,21 +60,21 @@ func Test_Provider_GetConnection(t *testing.T) {
},
}.WithDefaults(provider),
connection: models.Connection{
Type: vpn.OpenVPN,
Type: constants.OpenVPN,
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
Port: 1194,
Protocol: constants.UDP,
},
},
"default Wireguard port": {
filteredServers: []models.Server{
filteredServers: []umodels.Server{
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, WgPubKey: "x"},
},
selection: settings.ServerSelection{
VPN: vpn.Wireguard,
VPN: constants.Wireguard,
}.WithDefaults(provider),
connection: models.Connection{
Type: vpn.Wireguard,
Type: constants.Wireguard,
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
Port: 58237,
Protocol: constants.UDP,
@@ -92,8 +93,8 @@ func Test_Provider_GetConnection(t *testing.T) {
Return(testCase.filteredServers, testCase.storageErr)
client := (*http.Client)(nil)
warner := (common.Warner)(nil)
parallelResolver := (common.ParallelResolver)(nil)
warner := (updaters.Warner)(nil)
parallelResolver := (updaters.ParallelResolver)(nil)
provider := New(storage, client, warner, parallelResolver)
connection, err := provider.GetConnection(testCase.selection, testCase.ipv6Supported)
+6 -5
View File
@@ -5,23 +5,24 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/ivpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/ivpn"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
func New(storage common.Storage, client *http.Client, updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
func New(storage common.Storage, client *http.Client, updaterWarner updaters.Warner,
parallelResolver updaters.ParallelResolver,
) *Provider {
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(client, updaterWarner, parallelResolver),
Fetcher: ivpn.New(client, updaterWarner, parallelResolver),
}
}
-60
View File
@@ -1,60 +0,0 @@
package updater
import (
"context"
"encoding/json"
"fmt"
"net/http"
)
type apiData struct {
Servers []apiServer `json:"servers"`
}
type apiServer struct {
Hostnames apiHostnames `json:"hostnames"`
IsActive bool `json:"is_active"`
Country string `json:"country"`
City string `json:"city"`
ISP string `json:"isp"`
WgPubKey string `json:"wg_public_key"`
}
type apiHostnames struct {
OpenVPN string `json:"openvpn"`
Wireguard string `json:"wireguard"`
}
func fetchAPI(ctx context.Context, client *http.Client) (
data apiData, err error,
) {
const url = "https://api.ivpn.net/v4/servers/stats"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return data, err
}
response, err := client.Do(request)
if err != nil {
return data, err
}
if response.StatusCode != http.StatusOK {
_ = response.Body.Close()
return data, fmt.Errorf("HTTP status code not OK: %d %s",
response.StatusCode, response.Status)
}
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&data); err != nil {
_ = response.Body.Close()
return data, fmt.Errorf("decoding response body: %w", err)
}
if err := response.Body.Close(); err != nil {
return data, fmt.Errorf("closing response body: %w", err)
}
return data, nil
}
@@ -1,94 +0,0 @@
package updater
import (
"context"
"errors"
"io"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_fetchAPI(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
responseStatus int
responseBody io.ReadCloser
data apiData
err error
}{
"http response status not ok": {
responseStatus: http.StatusNoContent,
err: errors.New("HTTP status code not OK: 204 No Content"),
},
"nil body": {
responseStatus: http.StatusOK,
err: errors.New("decoding response body: EOF"),
},
"no server": {
responseStatus: http.StatusOK,
responseBody: io.NopCloser(strings.NewReader(`{}`)),
},
"success": {
responseStatus: http.StatusOK,
responseBody: io.NopCloser(strings.NewReader(`{"servers":[
{"country":"Country1","city":"City A","isp":"xyz","is_active":true,"hostnames":{"openvpn":"hosta"}},
{"country":"Country2","city":"City B","isp":"abc","is_active":false,"hostnames":{"openvpn":"hostb"}}
]}`)),
data: apiData{
Servers: []apiServer{
{
Country: "Country1",
City: "City A",
IsActive: true,
ISP: "xyz",
Hostnames: apiHostnames{
OpenVPN: "hosta",
},
},
{
Country: "Country2",
City: "City B",
ISP: "abc",
Hostnames: apiHostnames{
OpenVPN: "hostb",
},
},
},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, r.URL.String(), "https://api.ivpn.net/v4/servers/stats")
return &http.Response{
StatusCode: testCase.responseStatus,
Status: http.StatusText(testCase.responseStatus),
Body: testCase.responseBody,
}, nil
}),
}
data, err := fetchAPI(ctx, client)
assert.Equal(t, testCase.data, data)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
-28
View File
@@ -1,28 +0,0 @@
package updater
import (
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
const (
maxFailRatio = 0.1
maxDuration = 20 * time.Second
betweenDuration = time.Second
maxNoNew = 2
maxFails = 2
)
return resolver.ParallelSettings{
Hosts: hosts,
MaxFailRatio: maxFailRatio,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
BetweenDuration: betweenDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
}
@@ -1,9 +0,0 @@
package updater
import "net/http"
type roundTripFunc func(r *http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return f(r)
}
-110
View File
@@ -1,110 +0,0 @@
package updater
import (
"context"
"fmt"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/constants/vpn"
"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,
) {
data, err := fetchAPI(ctx, u.client)
if err != nil {
return nil, fmt.Errorf("fetching API: %w", err)
}
hosts := make(map[string]struct{}, len(data.Servers))
for _, serverData := range data.Servers {
openVPNHost := serverData.Hostnames.OpenVPN
if openVPNHost != "" {
hosts[openVPNHost] = struct{}{}
}
wireguardHost := serverData.Hostnames.Wireguard
if wireguardHost != "" {
hosts[wireguardHost] = struct{}{}
}
}
if len(hosts) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(hosts), minServers)
}
hostsSlice := make(sort.StringSlice, 0, len(hosts))
for host := range hosts {
hostsSlice = append(hostsSlice, host)
}
hostsSlice.Sort() // for predictable unit tests
resolveSettings := parallelResolverSettings(hostsSlice)
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
for _, warning := range warnings {
u.warner.Warn(warning)
}
if err != nil {
return nil, err
}
if len(hostToIPs) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
servers = make([]models.Server, 0, len(hostToIPs))
for _, serverData := range data.Servers {
city, region := parseCity(serverData.City)
server := models.Server{
Country: serverData.Country,
City: city,
Region: region,
ISP: serverData.ISP,
}
openVPNHostname := serverData.Hostnames.OpenVPN
wireguardHostname := serverData.Hostnames.Wireguard
if openVPNHostname == "" && wireguardHostname == "" {
warning := fmt.Sprintf("server data %v has no OpenVPN nor Wireguard hostname", serverData)
warnings = append(warnings, warning)
continue
}
if openVPNHostname != "" {
openVPNServer := server
openVPNServer.Hostname = openVPNHostname
openVPNServer.VPN = vpn.OpenVPN
openVPNServer.UDP = true
openVPNServer.TCP = true
openVPNServer.IPs = hostToIPs[openVPNHostname]
servers = append(servers, openVPNServer)
}
if wireguardHostname != "" {
wireguardServer := server
wireguardServer.Hostname = wireguardHostname
wireguardServer.VPN = vpn.Wireguard
wireguardServer.IPs = hostToIPs[wireguardHostname]
wireguardServer.WgPubKey = serverData.WgPubKey
servers = append(servers, wireguardServer)
}
}
sort.Sort(models.SortableServers(servers))
return servers, nil
}
func parseCity(city string) (parsedCity, region string) {
commaIndex := strings.Index(city, ", ")
if commaIndex == -1 {
return city, ""
}
return city[:commaIndex], city[commaIndex+2:]
}
@@ -1,183 +0,0 @@
package updater
import (
"context"
"errors"
"io"
"net/http"
"net/netip"
"strings"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Updater_GetServers(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
// Inputs
minServers int
// Mocks
warnerBuilder func(ctrl *gomock.Controller) common.Warner
// From API
responseBody string
responseStatus int
// Resolution
expectResolve bool
resolveSettings resolver.ParallelSettings
hostToIPs map[string][]netip.Addr
resolveWarnings []string
resolveErr error
// Output
servers []models.Server
err error
}{
"http response error": {
warnerBuilder: func(_ *gomock.Controller) common.Warner { return nil },
responseStatus: http.StatusNoContent,
err: errors.New("fetching API: HTTP status code not OK: 204 No Content"),
},
"resolve error": {
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
warner := common.NewMockWarner(ctrl)
warner.EXPECT().Warn("resolve warning")
return warner
},
responseBody: `{"servers":[
{"hostnames":{"openvpn":"hosta"}}
]}`,
responseStatus: http.StatusOK,
expectResolve: true,
resolveSettings: resolver.ParallelSettings{
Hosts: []string{"hosta"},
MaxFailRatio: 0.1,
Repeat: resolver.RepeatSettings{
MaxDuration: 20 * time.Second,
BetweenDuration: time.Second,
MaxNoNew: 2,
MaxFails: 2,
SortIPs: true,
},
},
resolveWarnings: []string{"resolve warning"},
resolveErr: errors.New("dummy"),
err: errors.New("dummy"),
},
"not enough servers": {
minServers: 2,
warnerBuilder: func(_ *gomock.Controller) common.Warner { return nil },
responseBody: `{"servers":[
{"hostnames":{"openvpn":"hosta"}}
]}`,
responseStatus: http.StatusOK,
err: errors.New("not enough servers found: 1 and expected at least 2"),
},
"success": {
minServers: 1,
warnerBuilder: func(ctrl *gomock.Controller) common.Warner {
warner := common.NewMockWarner(ctrl)
warner.EXPECT().Warn("resolve warning")
return warner
},
responseBody: `{"servers":[
{"country":"Country1","city":"City A","hostnames":{"openvpn":"hosta"}},
{"country":"Country2","city":"City B","hostnames":{"openvpn":"hostb"},"wg_public_key":"xyz"},
{"country":"Country3","city":"City C","hostnames":{"wireguard":"hostc"},"wg_public_key":"xyz"}
]}`,
responseStatus: http.StatusOK,
expectResolve: true,
resolveSettings: resolver.ParallelSettings{
Hosts: []string{"hosta", "hostb", "hostc"},
MaxFailRatio: 0.1,
Repeat: resolver.RepeatSettings{
MaxDuration: 20 * time.Second,
BetweenDuration: time.Second,
MaxNoNew: 2,
MaxFails: 2,
SortIPs: true,
},
},
hostToIPs: map[string][]netip.Addr{
"hosta": {netip.AddrFrom4([4]byte{1, 1, 1, 1}), netip.AddrFrom4([4]byte{2, 2, 2, 2})},
"hostb": {netip.AddrFrom4([4]byte{3, 3, 3, 3}), netip.AddrFrom4([4]byte{4, 4, 4, 4})},
"hostc": {netip.AddrFrom4([4]byte{5, 5, 5, 5}), netip.AddrFrom4([4]byte{6, 6, 6, 6})},
},
resolveWarnings: []string{"resolve warning"},
servers: []models.Server{
{
VPN: vpn.OpenVPN, Country: "Country1",
City: "City A", Hostname: "hosta", TCP: true, UDP: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1}), netip.AddrFrom4([4]byte{2, 2, 2, 2})},
},
{
VPN: vpn.OpenVPN, Country: "Country2",
City: "City B", Hostname: "hostb", TCP: true, UDP: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{3, 3, 3, 3}), netip.AddrFrom4([4]byte{4, 4, 4, 4})},
},
{
VPN: vpn.Wireguard,
Country: "Country3", City: "City C",
Hostname: "hostc",
WgPubKey: "xyz",
IPs: []netip.Addr{netip.AddrFrom4([4]byte{5, 5, 5, 5}), netip.AddrFrom4([4]byte{6, 6, 6, 6})},
},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := context.Background()
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, r.URL.String(), "https://api.ivpn.net/v4/servers/stats")
return &http.Response{
StatusCode: testCase.responseStatus,
Status: http.StatusText(testCase.responseStatus),
Body: io.NopCloser(strings.NewReader(testCase.responseBody)),
}, nil
}),
}
parallelResolver := common.NewMockParallelResolver(ctrl)
if testCase.expectResolve {
parallelResolver.EXPECT().Resolve(ctx, testCase.resolveSettings).
Return(testCase.hostToIPs, testCase.resolveWarnings, testCase.resolveErr)
}
warner := testCase.warnerBuilder(ctrl)
updater := &Updater{
client: client,
parallelResolver: parallelResolver,
warner: warner,
}
servers, err := updater.FetchServers(ctx, testCase.minServers)
assert.Equal(t, testCase.servers, servers)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
-23
View File
@@ -1,23 +0,0 @@
package updater
import (
"net/http"
"github.com/qdm12/gluetun/internal/provider/common"
)
type Updater struct {
client *http.Client
parallelResolver common.ParallelResolver
warner common.Warner
}
func New(client *http.Client, warner common.Warner,
parallelResolver common.ParallelResolver,
) *Updater {
return &Updater{
client: client,
parallelResolver: parallelResolver,
warner: warner,
}
}
+6 -6
View File
@@ -8,11 +8,11 @@ import (
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/pkg/updaters/constants"
umodels "github.com/qdm12/gluetun/pkg/updaters/models"
"github.com/stretchr/testify/assert"
)
@@ -22,7 +22,7 @@ func Test_Provider_GetConnection(t *testing.T) {
const provider = providers.Mullvad
testCases := map[string]struct {
filteredServers []models.Server
filteredServers []umodels.Server
storageErr error
selection settings.ServerSelection
ipv6Supported bool
@@ -34,14 +34,14 @@ func Test_Provider_GetConnection(t *testing.T) {
errMessage: "filtering servers: test error",
},
"default Wireguard port": {
filteredServers: []models.Server{
filteredServers: []umodels.Server{
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, WgPubKey: "x"},
},
selection: settings.ServerSelection{
VPN: vpn.Wireguard,
VPN: constants.Wireguard,
}.WithDefaults(provider),
connection: models.Connection{
Type: vpn.Wireguard,
Type: constants.Wireguard,
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
Port: 51820,
Protocol: constants.UDP,
+4 -3
View File
@@ -5,14 +5,15 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/mullvad/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
updaters "github.com/qdm12/gluetun/pkg/updaters/common"
"github.com/qdm12/gluetun/pkg/updaters/providers/mullvad"
)
type Provider struct {
storage common.Storage
connPicker *utils.ConnectionPicker
common.Fetcher
updaters.Fetcher
}
func New(storage common.Storage, client *http.Client,
@@ -20,7 +21,7 @@ func New(storage common.Storage, client *http.Client,
return &Provider{
storage: storage,
connPicker: utils.NewConnectionPicker(),
Fetcher: updater.New(client),
Fetcher: mullvad.New(client),
}
}
-51
View File
@@ -1,51 +0,0 @@
package updater
import (
"context"
"encoding/json"
"fmt"
"net/http"
)
type serverData struct {
Hostname string `json:"hostname"`
Country string `json:"country_name"`
City string `json:"city_name"`
Active bool `json:"active"`
Owned bool `json:"owned"`
Provider string `json:"provider"`
IPv4 string `json:"ipv4_addr_in"`
IPv6 string `json:"ipv6_addr_in"`
Type string `json:"type"`
PubKey string `json:"pubkey"` // Wireguard public key
}
func fetchAPI(ctx context.Context, client *http.Client) (data []serverData, err error) {
const url = "https://api.mullvad.net/www/relays/all/"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP status code not OK: %d %s", response.StatusCode, response.Status)
}
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&data); err != nil {
return nil, fmt.Errorf("failed decoding response body: %s", err)
}
if err := response.Body.Close(); err != nil {
return nil, err
}
return data, nil
}
@@ -1,78 +0,0 @@
package updater
import (
"errors"
"fmt"
"net/netip"
"strings"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
)
type hostToServer map[string]models.Server
func (hts hostToServer) add(data serverData) (err error) {
if !data.Active {
return nil
}
if data.IPv4 == "" && data.IPv6 == "" {
return errors.New("no IP address for VPN server")
}
server, ok := hts[data.Hostname]
if ok { // API returns a server per hostname at most
return nil
}
switch data.Type {
case "wireguard":
server.VPN = vpn.Wireguard
case "bridge":
// ignore bridge servers
return nil
default:
return fmt.Errorf("VPN type not supported: %s", data.Type)
}
if data.IPv4 != "" {
ipv4, err := netip.ParseAddr(data.IPv4)
if err != nil {
return fmt.Errorf("parsing IPv4 address: %w", err)
} else if !ipv4.Is4() {
return fmt.Errorf("IP address is not IPv4: %s", data.IPv4)
}
server.IPs = append(server.IPs, ipv4)
}
if data.IPv6 != "" {
ipv6, err := netip.ParseAddr(data.IPv6)
if err != nil {
return fmt.Errorf("parsing IPv6 address: %w", err)
} else if !ipv6.Is6() {
return fmt.Errorf("IP address is not IPv6: %s", data.IPv6)
}
server.IPs = append(server.IPs, ipv6)
}
server.Country = data.Country
server.City = strings.ReplaceAll(data.City, ",", "")
server.Hostname = data.Hostname
server.ISP = data.Provider
server.Owned = data.Owned
server.WgPubKey = data.PubKey
hts[data.Hostname] = server
return nil
}
func (hts hostToServer) toServersSlice() (servers []models.Server) {
servers = make([]models.Server, 0, len(hts))
for _, server := range hts {
server.IPs = uniqueSortedIPs(server.IPs)
servers = append(servers, server)
}
return servers
}
-32
View File
@@ -1,32 +0,0 @@
package updater
import (
"net/netip"
"sort"
)
func uniqueSortedIPs(ips []netip.Addr) []netip.Addr {
uniqueIPs := make(map[string]struct{}, len(ips))
for _, ip := range ips {
key := ip.String()
uniqueIPs[key] = struct{}{}
}
ips = make([]netip.Addr, 0, len(uniqueIPs))
for key := range uniqueIPs {
ip, err := netip.ParseAddr(key)
if err != nil {
panic(err)
}
if ip.Is4In6() {
ip = netip.AddrFrom4(ip.As4())
}
ips = append(ips, ip)
}
sort.Slice(ips, func(i, j int) bool {
return ips[i].Compare(ips[j]) < 0
})
return ips
}
@@ -1,40 +0,0 @@
package updater
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_uniqueSortedIPs(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
inputIPs []netip.Addr
outputIPs []netip.Addr
}{
"nil": {
inputIPs: nil,
outputIPs: []netip.Addr{},
},
"empty": {
inputIPs: []netip.Addr{},
outputIPs: []netip.Addr{},
},
"single IPv4": {
inputIPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})},
outputIPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})},
},
"two IPv4s": {
inputIPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 2, 1}), netip.AddrFrom4([4]byte{1, 1, 1, 1})},
outputIPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1}), netip.AddrFrom4([4]byte{1, 1, 2, 1})},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
outputIPs := uniqueSortedIPs(testCase.inputIPs)
assert.Equal(t, testCase.outputIPs, outputIPs)
})
}
}

Some files were not shown because too many files have changed in this diff Show More