mirror of
https://github.com/qdm12/gluetun.git
synced 2026-07-27 04:46:24 +02:00
chore(updater): move updater packages to pkg/updaters/<name>
This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package vpn
|
||||
|
||||
const (
|
||||
AmneziaWg = "amneziawg"
|
||||
OpenVPN = "openvpn"
|
||||
Wireguard = "wireguard"
|
||||
)
|
||||
@@ -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" +
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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("", " ")
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) (
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user