chore(all): move sub-packages to internal/provider
This commit is contained in:
269
internal/provider/cyberghost/updater/constants.go
Normal file
269
internal/provider/cyberghost/updater/constants.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package cyberghost
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/constants"
|
||||
|
||||
func getGroupIDToProtocol() map[string]string {
|
||||
return map[string]string{
|
||||
"87-1": constants.UDP, // Premium UDP Europe
|
||||
"94-1": constants.UDP, // Premium UDP USA
|
||||
"95-1": constants.UDP, // Premium UDP Asia
|
||||
"87-8": constants.UDP, // NoSpy UDP Europe
|
||||
"97-1": constants.TCP, // Premium TCP Europe
|
||||
"93-1": constants.TCP, // Premium TCP USA
|
||||
"96-1": constants.TCP, // Premium TCP Asia
|
||||
"97-8": constants.TCP, // NoSpy TCP Europe
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
}
|
||||
15
internal/provider/cyberghost/updater/countries.go
Normal file
15
internal/provider/cyberghost/updater/countries.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package cyberghost
|
||||
|
||||
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
|
||||
}
|
||||
69
internal/provider/cyberghost/updater/hosttoserver.go
Normal file
69
internal/provider/cyberghost/updater/hosttoserver.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package cyberghost
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"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][]net.IP) {
|
||||
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
|
||||
}
|
||||
42
internal/provider/cyberghost/updater/resolve.go
Normal file
42
internal/provider/cyberghost/updater/resolve.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package cyberghost
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
possibleHosts []string, minServers int) (
|
||||
hostToIPs map[string][]net.IP, err error) {
|
||||
const (
|
||||
maxFailRatio = 1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 4
|
||||
maxFails = 10
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
hostToIPs, _, err = presolver.Resolve(ctx, possibleHosts, settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hostToIPs, nil
|
||||
}
|
||||
28
internal/provider/cyberghost/updater/servers.go
Normal file
28
internal/provider/cyberghost/updater/servers.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Package cyberghost contains code to obtain the server information
|
||||
// for the Cyberghost provider.
|
||||
package cyberghost
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func GetServers(ctx context.Context, presolver resolver.Parallel,
|
||||
minServers int) (servers []models.Server, err error) {
|
||||
possibleServers := getPossibleServers()
|
||||
|
||||
possibleHosts := possibleServers.hostsSlice()
|
||||
hostToIPs, err := resolveHosts(ctx, presolver, possibleHosts, minServers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
possibleServers.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = possibleServers.toSlice()
|
||||
|
||||
sortServers(servers)
|
||||
return servers, nil
|
||||
}
|
||||
16
internal/provider/cyberghost/updater/sort.go
Normal file
16
internal/provider/cyberghost/updater/sort.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package cyberghost
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
152
internal/provider/expressvpn/updater/hardcoded.go
Normal file
152
internal/provider/expressvpn/updater/hardcoded.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package expressvpn
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
//nolint:lll
|
||||
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: "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: "Austria", Hostname: "austria-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: "Bhutan", Hostname: "bhutan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Bosnia And Herzegovina", City: "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: "Cambodia", Hostname: "cambodia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Canada", City: "Montreal", Hostname: "canada-montreal-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: "Canada", City: "Vancouver", Hostname: "canada-vancouver-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", City: "Costa Rica", Hostname: "costarica-ca-version-2.expressnetw.com"},
|
||||
{Country: "Croatia", Hostname: "croatia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Cyprus", Hostname: "cyprus-ca-version-2.expressnetw.com"},
|
||||
{Country: "Czech Republic", City: "Czech Republic", Hostname: "czechrepublic-ca-version-2.expressnetw.com"},
|
||||
{Country: "Denmark", Hostname: "denmark-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: "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-frankfurt-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "Germany", City: "Frankfurt", Hostname: "germany-frankfurt-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Germany", City: "Frankfurt", Hostname: "germany-darmstadt-ca-version-2.expressnetw.com"},
|
||||
{Country: "Germany", City: "Nuremberg", Hostname: "germany-nuremberg-ca-version-2.expressnetw.com"},
|
||||
{Country: "Greece", Hostname: "greece-ca-version-2.expressnetw.com"},
|
||||
{Country: "Guatemala", Hostname: "guatemala-ca-version-2.expressnetw.com"},
|
||||
{Country: "Hong Kong", City: "Hong Kong", Hostname: "hongkong-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Hong Kong", City: "Hong Kong", Hostname: "hongkong4-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", City: "Chennai", Hostname: "india-chennai-ca-version-2.expressnetw.com"},
|
||||
{Country: "India", City: "Mumbai", Hostname: "india-mumbai-1-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", City: "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: "Japan", City: "Kawasaki", Hostname: "japan-kawasaki-ca-version-2.expressnetw.com"},
|
||||
{Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-1-ca-version-2.expressnetw.com"},
|
||||
{Country: "Japan", City: "Tokyo", Hostname: "japan-tokyo-2-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: "Kyrgyzstan", Hostname: "kyrgyzstan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Laos", Hostname: "laos-ca-version-2.expressnetw.com"},
|
||||
{Country: "Latvia", Hostname: "latvia-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: "Myanmar", Hostname: "myanmar-ca-version-2.expressnetw.com"},
|
||||
{Country: "Nepal", Hostname: "nepal-ca-version-2.expressnetw.com"},
|
||||
{Country: "Netherlands", City: "Amsterdam", Hostname: "netherlands-amsterdam-2-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", City: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"},
|
||||
{Country: "North Macedonia", City: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Norway", Hostname: "norway-ca-version-2.expressnetw.com"},
|
||||
{Country: "Pakistan", Hostname: "pakistan-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", City: "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: "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", City: "South Africa", Hostname: "southafrica-ca-version-2.expressnetw.com"},
|
||||
{Country: "South Korea", City: "South Korea", Hostname: "southkorea2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Spain", City: "Barcelona", Hostname: "spain-barcelona-ca-version-2.expressnetw.com"},
|
||||
{Country: "Spain", City: "Madrid", Hostname: "spain-ca-version-2.expressnetw.com"},
|
||||
{Country: "Sri Lanka", City: "Sri Lanka", Hostname: "srilanka-ca-version-2.expressnetw.com"},
|
||||
{Country: "Sweden", Hostname: "sweden-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-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "Thailand", Hostname: "thailand-ca-version-2.expressnetw.com"},
|
||||
{Country: "Turkey", Hostname: "turkey-ca-version-2.expressnetw.com"},
|
||||
{Country: "Ukraine", Hostname: "ukraine-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "Docklands", Hostname: "uk-berkshire-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "London", Hostname: "uk-east-london-ca-version-2.expressnetw.com"},
|
||||
{Country: "UK", City: "London", Hostname: "uk-london-ca-version-2.expressnetw.com"},
|
||||
{Country: "Uruguay", Hostname: "uruguay-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Atlanta", Hostname: "usa-atlanta-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Chicago", Hostname: "usa-chicago-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Dallas", Hostname: "usa-dallas-2-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: "Los Angeles", Hostname: "usa-losangeles-1-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-losangeles5-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "Los Angeles", Hostname: "usa-losangeles-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-newjersey2-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New Jersey", Hostname: "usa-newjersey-3-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New York", Hostname: "us-new-york-2-ca-version-2.expressnetw.com"},
|
||||
{Country: "USA", City: "New York", Hostname: "usa-newyork-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: "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: "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"},
|
||||
}
|
||||
}
|
||||
30
internal/provider/expressvpn/updater/resolve.go
Normal file
30
internal/provider/expressvpn/updater/resolve.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package expressvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxNoNew = 1
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: time.Second,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
56
internal/provider/expressvpn/updater/servers.go
Normal file
56
internal/provider/expressvpn/updater/servers.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// package expressvpn contains code to obtain the server information
|
||||
// for the ExpressVPN provider.
|
||||
package expressvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
|
||||
presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
servers = hardcodedServers()
|
||||
|
||||
hosts := make([]string, len(servers))
|
||||
for i := range servers {
|
||||
hosts[i] = servers[i].Hostname
|
||||
}
|
||||
|
||||
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, 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, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
19
internal/provider/expressvpn/updater/sort.go
Normal file
19
internal/provider/expressvpn/updater/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package expressvpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].City == servers[j].City {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
39
internal/provider/fastestvpn/updater/filename.go
Normal file
39
internal/provider/fastestvpn/updater/filename.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package fastestvpn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var errFilenameNoProtocolSuffix = errors.New("filename does not have a protocol suffix")
|
||||
|
||||
var trailNumberExp = regexp.MustCompile(`[0-9]+$`)
|
||||
|
||||
func parseFilename(fileName string) (
|
||||
country string, tcp, udp bool, err error,
|
||||
) {
|
||||
const (
|
||||
tcpSuffix = "-TCP.ovpn"
|
||||
udpSuffix = "-UDP.ovpn"
|
||||
)
|
||||
var suffix string
|
||||
switch {
|
||||
case strings.HasSuffix(fileName, tcpSuffix):
|
||||
suffix = tcpSuffix
|
||||
tcp = true
|
||||
case strings.HasSuffix(fileName, udpSuffix):
|
||||
suffix = udpSuffix
|
||||
udp = true
|
||||
default:
|
||||
return "", false, false, fmt.Errorf("%w: %s",
|
||||
errFilenameNoProtocolSuffix, fileName)
|
||||
}
|
||||
|
||||
countryWithNumber := strings.TrimSuffix(fileName, suffix)
|
||||
number := trailNumberExp.FindString(countryWithNumber)
|
||||
country = countryWithNumber[:len(countryWithNumber)-len(number)]
|
||||
|
||||
return country, tcp, udp, nil
|
||||
}
|
||||
55
internal/provider/fastestvpn/updater/hosttoserver.go
Normal file
55
internal/provider/fastestvpn/updater/hosttoserver.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package fastestvpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"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 string, tcp, udp bool) {
|
||||
server, ok := hts[host]
|
||||
if !ok {
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.Hostname = host
|
||||
server.Country = country
|
||||
}
|
||||
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][]net.IP) {
|
||||
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
|
||||
}
|
||||
30
internal/provider/fastestvpn/updater/resolve.go
Normal file
30
internal/provider/fastestvpn/updater/resolve.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package fastestvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxNoNew = 1
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: time.Second,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
82
internal/provider/fastestvpn/updater/servers.go
Normal file
82
internal/provider/fastestvpn/updater/servers.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Package fastestvpn contains code to obtain the server information
|
||||
// for the FastestVPN provider.
|
||||
package fastestvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
|
||||
presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
const url = "https://support.fastestvpn.com/download/openvpn-tcp-udp-config-files"
|
||||
contents, err := unzipper.FetchAndExtract(ctx, url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(contents) < minServers {
|
||||
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(contents), minServers)
|
||||
}
|
||||
|
||||
hts := make(hostToServer)
|
||||
|
||||
for fileName, content := range contents {
|
||||
if !strings.HasSuffix(fileName, ".ovpn") {
|
||||
continue // not an OpenVPN file
|
||||
}
|
||||
|
||||
country, tcp, udp, err := parseFilename(fileName)
|
||||
if err != nil {
|
||||
warnings = append(warnings, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
host, warning, err := openvpn.ExtractHost(content)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
hts.add(host, country, tcp, udp)
|
||||
}
|
||||
|
||||
if len(hts) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(hts), minServers)
|
||||
}
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
16
internal/provider/fastestvpn/updater/sort.go
Normal file
16
internal/provider/fastestvpn/updater/sort.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package fastestvpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
19
internal/provider/hidemyass/updater/hosts.go
Normal file
19
internal/provider/hidemyass/updater/hosts.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package hidemyass
|
||||
|
||||
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
|
||||
}
|
||||
37
internal/provider/hidemyass/updater/hosttourl.go
Normal file
37
internal/provider/hidemyass/updater/hosttourl.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package hidemyass
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return openvpn.FetchMultiFiles(ctx, client, urls)
|
||||
}
|
||||
54
internal/provider/hidemyass/updater/index.go
Normal file
54
internal/provider/hidemyass/updater/index.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package hidemyass
|
||||
|
||||
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
|
||||
}
|
||||
33
internal/provider/hidemyass/updater/resolve.go
Normal file
33
internal/provider/hidemyass/updater/resolve.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package hidemyass
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (
|
||||
hostToIPs map[string][]net.IP, warnings []string, err error) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 15 * time.Second
|
||||
betweenDuration = 2 * time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
70
internal/provider/hidemyass/updater/servers.go
Normal file
70
internal/provider/hidemyass/updater/servers.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Package hidemyass contains code to obtain the server information
|
||||
// for the HideMyAss provider.
|
||||
package hidemyass
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, client *http.Client,
|
||||
presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
tcpHostToURL, udpHostToURL, err := getAllHostToURL(ctx, client)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
hosts := getUniqueHosts(tcpHostToURL, udpHostToURL)
|
||||
|
||||
if len(hosts) < minServers {
|
||||
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(hosts), minServers)
|
||||
}
|
||||
|
||||
hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
22
internal/provider/hidemyass/updater/sort.go
Normal file
22
internal/provider/hidemyass/updater/sort.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package hidemyass
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].Region == servers[j].Region {
|
||||
if servers[i].City == servers[j].City {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Region < servers[j].Region
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
58
internal/provider/hidemyass/updater/url.go
Normal file
58
internal/provider/hidemyass/updater/url.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package hidemyass
|
||||
|
||||
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:gomnd
|
||||
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
|
||||
}
|
||||
}
|
||||
40
internal/provider/ipvanish/updater/filename.go
Normal file
40
internal/provider/ipvanish/updater/filename.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package ipvanish
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"golang.org/x/text/cases"
|
||||
)
|
||||
|
||||
var errCountryCodeUnknown = errors.New("country code is unknown")
|
||||
|
||||
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("%w: %s", errCountryCodeUnknown, countryCode)
|
||||
}
|
||||
country = titleCaser.String(country)
|
||||
|
||||
if len(parts) > 1 {
|
||||
city = strings.Join(parts[1:], " ")
|
||||
city = titleCaser.String(city)
|
||||
}
|
||||
|
||||
return country, city, nil
|
||||
}
|
||||
57
internal/provider/ipvanish/updater/filename_test.go
Normal file
57
internal/provider/ipvanish/updater/filename_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package ipvanish
|
||||
|
||||
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 {
|
||||
testCase := testCase
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
60
internal/provider/ipvanish/updater/hosttoserver.go
Normal file
60
internal/provider/ipvanish/updater/hosttoserver.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package ipvanish
|
||||
|
||||
import (
|
||||
"net"
|
||||
"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][]net.IP) {
|
||||
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
|
||||
}
|
||||
216
internal/provider/ipvanish/updater/hosttoserver_test.go
Normal file
216
internal/provider/ipvanish/updater/hosttoserver_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package ipvanish
|
||||
|
||||
import (
|
||||
"net"
|
||||
"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 {
|
||||
testCase := testCase
|
||||
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 {
|
||||
testCase := testCase
|
||||
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][]net.IP
|
||||
expectedHTS hostToServer
|
||||
}{
|
||||
"create server": {
|
||||
initialHTS: hostToServer{},
|
||||
hostToIPs: map[string][]net.IP{
|
||||
"A": {{1, 2, 3, 4}},
|
||||
},
|
||||
expectedHTS: hostToServer{
|
||||
"A": models.Server{
|
||||
IPs: []net.IP{{1, 2, 3, 4}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"add IPs to existing server": {
|
||||
initialHTS: hostToServer{
|
||||
"A": models.Server{
|
||||
Country: "country",
|
||||
},
|
||||
},
|
||||
hostToIPs: map[string][]net.IP{
|
||||
"A": {{1, 2, 3, 4}},
|
||||
},
|
||||
expectedHTS: hostToServer{
|
||||
"A": models.Server{
|
||||
Country: "country",
|
||||
IPs: []net.IP{{1, 2, 3, 4}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"remove server without IP": {
|
||||
initialHTS: hostToServer{
|
||||
"A": models.Server{
|
||||
Country: "country",
|
||||
},
|
||||
},
|
||||
hostToIPs: map[string][]net.IP{},
|
||||
expectedHTS: hostToServer{},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
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 {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
servers := testCase.hts.toServersSlice()
|
||||
assert.ElementsMatch(t, testCase.servers, servers)
|
||||
})
|
||||
}
|
||||
}
|
||||
37
internal/provider/ipvanish/updater/resolve.go
Normal file
37
internal/provider/ipvanish/updater/resolve.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package ipvanish
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func getResolveSettings(minServers int) (settings resolver.ParallelSettings) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
return resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
settings := getResolveSettings(minServers)
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
57
internal/provider/ipvanish/updater/resolve_test.go
Normal file
57
internal/provider/ipvanish/updater/resolve_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package ipvanish
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_resolveHosts(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
ctx := context.Background()
|
||||
presolver := mock_resolver.NewMockParallel(ctrl)
|
||||
hosts := []string{"host1", "host2"}
|
||||
const minServers = 10
|
||||
|
||||
expectedHostToIPs := map[string][]net.IP{
|
||||
"host1": {{1, 2, 3, 4}},
|
||||
"host2": {{2, 3, 4, 5}},
|
||||
}
|
||||
expectedWarnings := []string{"warning1", "warning2"}
|
||||
expectedErr := errors.New("dummy")
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
expectedSettings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
presolver.EXPECT().Resolve(ctx, hosts, expectedSettings).
|
||||
Return(expectedHostToIPs, expectedWarnings, expectedErr)
|
||||
|
||||
hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
assert.Equal(t, expectedHostToIPs, hostToIPs)
|
||||
assert.Equal(t, expectedWarnings, warnings)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
}
|
||||
95
internal/provider/ipvanish/updater/servers.go
Normal file
95
internal/provider/ipvanish/updater/servers.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Package ipvanish contains code to obtain the server information
|
||||
// for the Surshark provider.
|
||||
package ipvanish
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
|
||||
presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
const url = "https://www.ipvanish.com/software/configs/configs.zip"
|
||||
contents, err := unzipper.FetchAndExtract(ctx, url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(contents) < minServers {
|
||||
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
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
|
||||
warning := err.Error() + ": in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
hostname, warning, err := openvpn.ExtractHost(content)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
country, city, err := parseFilename(fileName, hostname, titleCaser)
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
hts.add(hostname, country, city, tcp, udp)
|
||||
}
|
||||
|
||||
if len(hts) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(hts), minServers)
|
||||
}
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
165
internal/provider/ipvanish/updater/servers_test.go
Normal file
165
internal/provider/ipvanish/updater/servers_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package ipvanish
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip/mock_unzip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
// Inputs
|
||||
minServers int
|
||||
|
||||
// Unzip
|
||||
unzipContents map[string][]byte
|
||||
unzipErr error
|
||||
|
||||
// Resolution
|
||||
expectResolve bool
|
||||
hostsToResolve []string
|
||||
resolveSettings resolver.ParallelSettings
|
||||
hostToIPs map[string][]net.IP
|
||||
resolveWarnings []string
|
||||
resolveErr error
|
||||
|
||||
// Output
|
||||
servers []models.Server
|
||||
warnings []string
|
||||
err error
|
||||
}{
|
||||
"unzipper error": {
|
||||
unzipErr: errors.New("dummy"),
|
||||
err: errors.New("dummy"),
|
||||
},
|
||||
"not enough unzip contents": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"no openvpn file": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{"somefile.txt": {}},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"invalid proto": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{"badproto.ovpn": []byte(`proto invalid`)},
|
||||
warnings: []string{"unknown protocol: invalid: in badproto.ovpn"},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"no host": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{"nohost.ovpn": []byte(``)},
|
||||
warnings: []string{"remote host not found in nohost.ovpn"},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"multiple hosts": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{
|
||||
"ipvanish-CA-City-A-hosta.ovpn": []byte("remote hosta\nremote hostb"),
|
||||
},
|
||||
expectResolve: true,
|
||||
hostsToResolve: []string{"hosta"},
|
||||
resolveSettings: getResolveSettings(1),
|
||||
warnings: []string{"only using the first host \"hosta\" and discarding 1 other hosts"},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"resolve error": {
|
||||
unzipContents: map[string][]byte{
|
||||
"ipvanish-CA-City-A-hosta.ovpn": []byte("remote hosta"),
|
||||
},
|
||||
expectResolve: true,
|
||||
hostsToResolve: []string{"hosta"},
|
||||
resolveSettings: getResolveSettings(0),
|
||||
resolveWarnings: []string{"resolve warning"},
|
||||
resolveErr: errors.New("dummy"),
|
||||
warnings: []string{"resolve warning"},
|
||||
err: errors.New("dummy"),
|
||||
},
|
||||
"filename parsing error": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{
|
||||
"ipvanish-unknown-City-A-hosta.ovpn": []byte("remote hosta"),
|
||||
},
|
||||
warnings: []string{"country code is unknown: unknown in ipvanish-unknown-City-A-hosta.ovpn"},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"success": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{
|
||||
"ipvanish-CA-City-A-hosta.ovpn": []byte("remote hosta"),
|
||||
"ipvanish-LU-City-B-hostb.ovpn": []byte("remote hostb"),
|
||||
},
|
||||
expectResolve: true,
|
||||
hostsToResolve: []string{"hosta", "hostb"},
|
||||
resolveSettings: getResolveSettings(1),
|
||||
hostToIPs: map[string][]net.IP{
|
||||
"hosta": {{1, 1, 1, 1}, {2, 2, 2, 2}},
|
||||
"hostb": {{3, 3, 3, 3}, {4, 4, 4, 4}},
|
||||
},
|
||||
resolveWarnings: []string{"resolve warning"},
|
||||
servers: []models.Server{
|
||||
{
|
||||
VPN: vpn.OpenVPN,
|
||||
Country: "Canada",
|
||||
City: "City A",
|
||||
Hostname: "hosta",
|
||||
UDP: true,
|
||||
IPs: []net.IP{{1, 1, 1, 1}, {2, 2, 2, 2}},
|
||||
},
|
||||
{
|
||||
VPN: vpn.OpenVPN,
|
||||
Country: "Luxembourg",
|
||||
City: "City B",
|
||||
Hostname: "hostb",
|
||||
UDP: true,
|
||||
IPs: []net.IP{{3, 3, 3, 3}, {4, 4, 4, 4}},
|
||||
},
|
||||
},
|
||||
warnings: []string{"resolve warning"},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
unzipper := mock_unzip.NewMockUnzipper(ctrl)
|
||||
const zipURL = "https://www.ipvanish.com/software/configs/configs.zip"
|
||||
unzipper.EXPECT().FetchAndExtract(ctx, zipURL).
|
||||
Return(testCase.unzipContents, testCase.unzipErr)
|
||||
|
||||
presolver := mock_resolver.NewMockParallel(ctrl)
|
||||
if testCase.expectResolve {
|
||||
presolver.EXPECT().Resolve(ctx, testCase.hostsToResolve, testCase.resolveSettings).
|
||||
Return(testCase.hostToIPs, testCase.resolveWarnings, testCase.resolveErr)
|
||||
}
|
||||
|
||||
servers, warnings, err := GetServers(ctx, unzipper, presolver, testCase.minServers)
|
||||
|
||||
assert.Equal(t, testCase.servers, servers)
|
||||
assert.Equal(t, testCase.warnings, warnings)
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
19
internal/provider/ipvanish/updater/sort.go
Normal file
19
internal/provider/ipvanish/updater/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package ipvanish
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].City == servers[j].City {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
40
internal/provider/ipvanish/updater/sort_test.go
Normal file
40
internal/provider/ipvanish/updater/sort_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package ipvanish
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_sortServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
initialServers []models.Server
|
||||
sortedServers []models.Server
|
||||
}{
|
||||
"no server": {},
|
||||
"sorted servers": {
|
||||
initialServers: []models.Server{
|
||||
{Country: "B", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "A", Hostname: "B"},
|
||||
{Country: "A", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "B", Hostname: "A"},
|
||||
},
|
||||
sortedServers: []models.Server{
|
||||
{Country: "A", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "A", Hostname: "B"},
|
||||
{Country: "A", City: "B", Hostname: "A"},
|
||||
{Country: "B", City: "A", Hostname: "A"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sortServers(testCase.initialServers)
|
||||
assert.Equal(t, testCase.sortedServers, testCase.initialServers)
|
||||
})
|
||||
}
|
||||
}
|
||||
64
internal/provider/ivpn/updater/api.go
Normal file
64
internal/provider/ivpn/updater/api.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
errHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
)
|
||||
|
||||
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("%w: %d %s",
|
||||
errHTTPStatusCodeNotOK, response.StatusCode, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
_ = response.Body.Close()
|
||||
return data, fmt.Errorf("failed unmarshaling response body: %w", err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return data, fmt.Errorf("cannot close response body: %w", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
96
internal/provider/ivpn/updater/api_test.go
Normal file
96
internal/provider/ivpn/updater/api_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"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("failed unmarshaling response body: EOF"),
|
||||
},
|
||||
"no server": {
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`{}`)),
|
||||
},
|
||||
"success": {
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.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 {
|
||||
testCase := testCase
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
37
internal/provider/ivpn/updater/resolve.go
Normal file
37
internal/provider/ivpn/updater/resolve.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func getResolveSettings(minServers int) (settings resolver.ParallelSettings) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
return resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
settings := getResolveSettings(minServers)
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
57
internal/provider/ivpn/updater/resolve_test.go
Normal file
57
internal/provider/ivpn/updater/resolve_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_resolveHosts(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
ctx := context.Background()
|
||||
presolver := mock_resolver.NewMockParallel(ctrl)
|
||||
hosts := []string{"host1", "host2"}
|
||||
const minServers = 10
|
||||
|
||||
expectedHostToIPs := map[string][]net.IP{
|
||||
"host1": {{1, 2, 3, 4}},
|
||||
"host2": {{2, 3, 4, 5}},
|
||||
}
|
||||
expectedWarnings := []string{"warning1", "warning2"}
|
||||
expectedErr := errors.New("dummy")
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
expectedSettings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
presolver.EXPECT().Resolve(ctx, hosts, expectedSettings).
|
||||
Return(expectedHostToIPs, expectedWarnings, expectedErr)
|
||||
|
||||
hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
assert.Equal(t, expectedHostToIPs, hostToIPs)
|
||||
assert.Equal(t, expectedWarnings, warnings)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
}
|
||||
9
internal/provider/ivpn/updater/roundtrip_test.go
Normal file
9
internal/provider/ivpn/updater/roundtrip_test.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package ivpn
|
||||
|
||||
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)
|
||||
}
|
||||
82
internal/provider/ivpn/updater/servers.go
Normal file
82
internal/provider/ivpn/updater/servers.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Package ivpn contains code to obtain the server information
|
||||
// for the Surshark provider.
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
)
|
||||
|
||||
func GetServers(ctx context.Context, client *http.Client,
|
||||
presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
data, err := fetchAPI(ctx, client)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed fetching API: %w", err)
|
||||
}
|
||||
|
||||
hosts := make([]string, 0, len(data.Servers))
|
||||
|
||||
for _, serverData := range data.Servers {
|
||||
openVPNHost := serverData.Hostnames.OpenVPN
|
||||
if openVPNHost != "" {
|
||||
hosts = append(hosts, openVPNHost)
|
||||
}
|
||||
|
||||
wireguardHost := serverData.Hostnames.Wireguard
|
||||
if wireguardHost != "" {
|
||||
hosts = append(hosts, wireguardHost)
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) < minServers {
|
||||
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(hosts), minServers)
|
||||
}
|
||||
|
||||
hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
servers = make([]models.Server, 0, len(hosts))
|
||||
for _, serverData := range data.Servers {
|
||||
vpnType := vpn.OpenVPN
|
||||
hostname := serverData.Hostnames.OpenVPN
|
||||
tcp := true
|
||||
wgPubKey := ""
|
||||
if hostname == "" {
|
||||
vpnType = vpn.Wireguard
|
||||
hostname = serverData.Hostnames.Wireguard
|
||||
tcp = false
|
||||
wgPubKey = serverData.WgPubKey
|
||||
}
|
||||
|
||||
server := models.Server{
|
||||
VPN: vpnType,
|
||||
Country: serverData.Country,
|
||||
City: serverData.City,
|
||||
ISP: serverData.ISP,
|
||||
Hostname: hostname,
|
||||
WgPubKey: wgPubKey,
|
||||
TCP: tcp,
|
||||
UDP: true,
|
||||
IPs: hostToIPs[hostname],
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
141
internal/provider/ivpn/updater/servers_test.go
Normal file
141
internal/provider/ivpn/updater/servers_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
// Inputs
|
||||
minServers int
|
||||
|
||||
// From API
|
||||
responseBody string
|
||||
responseStatus int
|
||||
|
||||
// Resolution
|
||||
expectResolve bool
|
||||
hostsToResolve []string
|
||||
resolveSettings resolver.ParallelSettings
|
||||
hostToIPs map[string][]net.IP
|
||||
resolveWarnings []string
|
||||
resolveErr error
|
||||
|
||||
// Output
|
||||
servers []models.Server
|
||||
warnings []string
|
||||
err error
|
||||
}{
|
||||
"http response error": {
|
||||
responseStatus: http.StatusNoContent,
|
||||
err: errors.New("failed fetching API: HTTP status code not OK: 204 No Content"),
|
||||
},
|
||||
"resolve error": {
|
||||
responseBody: `{"servers":[
|
||||
{"hostnames":{"openvpn":"hosta"}}
|
||||
]}`,
|
||||
responseStatus: http.StatusOK,
|
||||
expectResolve: true,
|
||||
hostsToResolve: []string{"hosta"},
|
||||
resolveSettings: getResolveSettings(0),
|
||||
resolveWarnings: []string{"resolve warning"},
|
||||
resolveErr: errors.New("dummy"),
|
||||
warnings: []string{"resolve warning"},
|
||||
err: errors.New("dummy"),
|
||||
},
|
||||
"not enough servers": {
|
||||
minServers: 2,
|
||||
responseBody: `{"servers":[
|
||||
{"hostnames":{"openvpn":"hosta"}}
|
||||
]}`,
|
||||
responseStatus: http.StatusOK,
|
||||
err: errors.New("not enough servers found: 1 and expected at least 2"),
|
||||
},
|
||||
"success": {
|
||||
minServers: 1,
|
||||
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,
|
||||
hostsToResolve: []string{"hosta", "hostb", "hostc"},
|
||||
resolveSettings: getResolveSettings(1),
|
||||
hostToIPs: map[string][]net.IP{
|
||||
"hosta": {{1, 1, 1, 1}, {2, 2, 2, 2}},
|
||||
"hostb": {{3, 3, 3, 3}, {4, 4, 4, 4}},
|
||||
"hostc": {{5, 5, 5, 5}, {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: []net.IP{{1, 1, 1, 1}, {2, 2, 2, 2}}},
|
||||
{VPN: vpn.OpenVPN, Country: "Country2",
|
||||
City: "City B", Hostname: "hostb", TCP: true, UDP: true,
|
||||
IPs: []net.IP{{3, 3, 3, 3}, {4, 4, 4, 4}}},
|
||||
{VPN: vpn.Wireguard,
|
||||
Country: "Country3", City: "City C",
|
||||
Hostname: "hostc", UDP: true,
|
||||
WgPubKey: "xyz",
|
||||
IPs: []net.IP{{5, 5, 5, 5}, {6, 6, 6, 6}}},
|
||||
},
|
||||
warnings: []string{"resolve warning"},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
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: ioutil.NopCloser(strings.NewReader(testCase.responseBody)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
presolver := mock_resolver.NewMockParallel(ctrl)
|
||||
if testCase.expectResolve {
|
||||
presolver.EXPECT().Resolve(ctx, testCase.hostsToResolve, testCase.resolveSettings).
|
||||
Return(testCase.hostToIPs, testCase.resolveWarnings, testCase.resolveErr)
|
||||
}
|
||||
|
||||
servers, warnings, err := GetServers(ctx, client, presolver, testCase.minServers)
|
||||
|
||||
assert.Equal(t, testCase.servers, servers)
|
||||
assert.Equal(t, testCase.warnings, warnings)
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
22
internal/provider/ivpn/updater/sort.go
Normal file
22
internal/provider/ivpn/updater/sort.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].City == servers[j].City {
|
||||
if servers[i].Hostname == servers[j].Hostname {
|
||||
return servers[i].VPN < servers[j].VPN
|
||||
}
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
40
internal/provider/ivpn/updater/sort_test.go
Normal file
40
internal/provider/ivpn/updater/sort_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_sortServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
initialServers []models.Server
|
||||
sortedServers []models.Server
|
||||
}{
|
||||
"no server": {},
|
||||
"sorted servers": {
|
||||
initialServers: []models.Server{
|
||||
{Country: "B", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "A", Hostname: "B"},
|
||||
{Country: "A", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "B", Hostname: "A"},
|
||||
},
|
||||
sortedServers: []models.Server{
|
||||
{Country: "A", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "A", Hostname: "B"},
|
||||
{Country: "A", City: "B", Hostname: "A"},
|
||||
{Country: "B", City: "A", Hostname: "A"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sortServers(testCase.initialServers)
|
||||
assert.Equal(t, testCase.sortedServers, testCase.initialServers)
|
||||
})
|
||||
}
|
||||
}
|
||||
58
internal/provider/mullvad/updater/api.go
Normal file
58
internal/provider/mullvad/updater/api.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
ErrUnmarshalResponseBody = errors.New("failed unmarshaling response body")
|
||||
)
|
||||
|
||||
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("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
||||
response.StatusCode, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponseBody, err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
86
internal/provider/mullvad/updater/hosttoserver.go
Normal file
86
internal/provider/mullvad/updater/hosttoserver.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.Server
|
||||
|
||||
var (
|
||||
ErrNoIP = errors.New("no IP address for VPN server")
|
||||
ErrParseIPv4 = errors.New("cannot parse IPv4 address")
|
||||
ErrParseIPv6 = errors.New("cannot parse IPv6 address")
|
||||
ErrVPNTypeNotSupported = errors.New("VPN type not supported")
|
||||
)
|
||||
|
||||
func (hts hostToServer) add(data serverData) (err error) {
|
||||
if !data.Active {
|
||||
return
|
||||
}
|
||||
|
||||
if data.IPv4 == "" && data.IPv6 == "" {
|
||||
return ErrNoIP
|
||||
}
|
||||
|
||||
server, ok := hts[data.Hostname]
|
||||
if ok { // API returns a server per hostname at most
|
||||
return nil
|
||||
}
|
||||
|
||||
switch data.Type {
|
||||
case "openvpn":
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.UDP = true
|
||||
server.TCP = true
|
||||
case "wireguard":
|
||||
server.VPN = vpn.Wireguard
|
||||
server.UDP = true
|
||||
case "bridge":
|
||||
// ignore bridge servers
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", ErrVPNTypeNotSupported, data.Type)
|
||||
}
|
||||
|
||||
if data.IPv4 != "" {
|
||||
ipv4 := net.ParseIP(data.IPv4)
|
||||
if ipv4 == nil || ipv4.To4() == nil {
|
||||
return fmt.Errorf("%w: %s", ErrParseIPv4, data.IPv4)
|
||||
}
|
||||
server.IPs = append(server.IPs, ipv4)
|
||||
}
|
||||
|
||||
if data.IPv6 != "" {
|
||||
ipv6 := net.ParseIP(data.IPv6)
|
||||
if ipv6 == nil || ipv6.To4() != nil {
|
||||
return fmt.Errorf("%w: %s", ErrParseIPv6, 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
|
||||
}
|
||||
30
internal/provider/mullvad/updater/ips.go
Normal file
30
internal/provider/mullvad/updater/ips.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func uniqueSortedIPs(ips []net.IP) []net.IP {
|
||||
uniqueIPs := make(map[string]struct{}, len(ips))
|
||||
for _, ip := range ips {
|
||||
key := ip.String()
|
||||
uniqueIPs[key] = struct{}{}
|
||||
}
|
||||
|
||||
ips = make([]net.IP, 0, len(uniqueIPs))
|
||||
for key := range uniqueIPs {
|
||||
ip := net.ParseIP(key)
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
ip = ipv4
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
|
||||
sort.Slice(ips, func(i, j int) bool {
|
||||
return bytes.Compare(ips[i], ips[j]) < 0
|
||||
})
|
||||
|
||||
return ips
|
||||
}
|
||||
41
internal/provider/mullvad/updater/ips_test.go
Normal file
41
internal/provider/mullvad/updater/ips_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_uniqueSortedIPs(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
inputIPs []net.IP
|
||||
outputIPs []net.IP
|
||||
}{
|
||||
"nil": {
|
||||
inputIPs: nil,
|
||||
outputIPs: []net.IP{},
|
||||
},
|
||||
"empty": {
|
||||
inputIPs: []net.IP{},
|
||||
outputIPs: []net.IP{},
|
||||
},
|
||||
"single IPv4": {
|
||||
inputIPs: []net.IP{{1, 1, 1, 1}},
|
||||
outputIPs: []net.IP{{1, 1, 1, 1}},
|
||||
},
|
||||
"two IPv4s": {
|
||||
inputIPs: []net.IP{{1, 1, 2, 1}, {1, 1, 1, 1}},
|
||||
outputIPs: []net.IP{{1, 1, 1, 1}, {1, 1, 2, 1}},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
outputIPs := uniqueSortedIPs(testCase.inputIPs)
|
||||
assert.Equal(t, testCase.outputIPs, outputIPs)
|
||||
})
|
||||
}
|
||||
}
|
||||
40
internal/provider/mullvad/updater/servers.go
Normal file
40
internal/provider/mullvad/updater/servers.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Package mullvad contains code to obtain the server information
|
||||
// for the Mullvad provider.
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, client *http.Client, minServers int) (
|
||||
servers []models.Server, err error) {
|
||||
data, err := fetchAPI(ctx, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hts := make(hostToServer)
|
||||
for _, serverData := range data {
|
||||
if err := hts.add(serverData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(hts) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(hts), minServers)
|
||||
}
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
22
internal/provider/mullvad/updater/sort.go
Normal file
22
internal/provider/mullvad/updater/sort.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].City == servers[j].City {
|
||||
if servers[i].Hostname == servers[j].Hostname {
|
||||
return servers[i].ISP < servers[j].ISP
|
||||
}
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
54
internal/provider/nordvpn/updater/api.go
Normal file
54
internal/provider/nordvpn/updater/api.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package nordvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
)
|
||||
|
||||
type serverData struct {
|
||||
Domain string `json:"domain"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Name string `json:"name"`
|
||||
Country string `json:"country"`
|
||||
Features struct {
|
||||
UDP bool `json:"openvpn_udp"`
|
||||
TCP bool `json:"openvpn_tcp"`
|
||||
} `json:"features"`
|
||||
}
|
||||
|
||||
func fetchAPI(ctx context.Context, client *http.Client) (data []serverData, err error) {
|
||||
const url = "https://nordvpn.com/api/server"
|
||||
|
||||
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("%w: %s", ErrHTTPStatusCodeNotOK, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return nil, fmt.Errorf("failed unmarshaling response body: %w", err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
16
internal/provider/nordvpn/updater/ip.go
Normal file
16
internal/provider/nordvpn/updater/ip.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package nordvpn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
func parseIPv4(s string) (ipv4 net.IP, err error) {
|
||||
ip := net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("%w: %q", ErrParseIP, s)
|
||||
} else if ip.To4() == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotIPv4, ip)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
29
internal/provider/nordvpn/updater/name.go
Normal file
29
internal/provider/nordvpn/updater/name.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package nordvpn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoIDInServerName = errors.New("no ID in server name")
|
||||
ErrInvalidIDInServerName = errors.New("invalid ID in server name")
|
||||
)
|
||||
|
||||
func parseServerName(serverName string) (number uint16, err error) {
|
||||
i := strings.IndexRune(serverName, '#')
|
||||
if i < 0 {
|
||||
return 0, fmt.Errorf("%w: %s", ErrNoIDInServerName, serverName)
|
||||
}
|
||||
|
||||
idString := serverName[i+1:]
|
||||
idUint64, err := strconv.ParseUint(idString, 10, 16) //nolint:gomnd
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: %s", ErrInvalidIDInServerName, serverName)
|
||||
}
|
||||
|
||||
number = uint16(idUint64)
|
||||
return number, nil
|
||||
}
|
||||
68
internal/provider/nordvpn/updater/servers.go
Normal file
68
internal/provider/nordvpn/updater/servers.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Package nordvpn contains code to obtain the server information
|
||||
// for the NordVPN provider.
|
||||
package nordvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrParseIP = errors.New("cannot parse IP address")
|
||||
ErrNotIPv4 = errors.New("IP address is not IPv4")
|
||||
ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
)
|
||||
|
||||
func GetServers(ctx context.Context, client *http.Client, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
data, err := fetchAPI(ctx, client)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
servers = make([]models.Server, 0, len(data))
|
||||
|
||||
for _, jsonServer := range data {
|
||||
if !jsonServer.Features.TCP && !jsonServer.Features.UDP {
|
||||
warning := "server does not support TCP and UDP for openvpn: " + jsonServer.Name
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
ip, err := parseIPv4(jsonServer.IPAddress)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%w for server %s", err, jsonServer.Name)
|
||||
}
|
||||
|
||||
number, err := parseServerName(jsonServer.Name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
server := models.Server{
|
||||
VPN: vpn.OpenVPN,
|
||||
Region: jsonServer.Country,
|
||||
Hostname: jsonServer.Domain,
|
||||
Number: number,
|
||||
IPs: []net.IP{ip},
|
||||
TCP: jsonServer.Features.TCP,
|
||||
UDP: jsonServer.Features.UDP,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
16
internal/provider/nordvpn/updater/sort.go
Normal file
16
internal/provider/nordvpn/updater/sort.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package nordvpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Region == servers[j].Region {
|
||||
return servers[i].Number < servers[j].Number
|
||||
}
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
}
|
||||
47
internal/provider/perfectprivacy/updater/citytoserver.go
Normal file
47
internal/provider/perfectprivacy/updater/citytoserver.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package perfectprivacy
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type cityToServer map[string]models.Server
|
||||
|
||||
func (cts cityToServer) add(city string, ips []net.IP) {
|
||||
server, ok := cts[city]
|
||||
if !ok {
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.City = city
|
||||
server.IPs = ips
|
||||
server.TCP = true
|
||||
server.UDP = true
|
||||
} else {
|
||||
// Do not insert duplicate IP addresses
|
||||
existingIPs := make(map[string]struct{}, len(server.IPs))
|
||||
for _, ip := range server.IPs {
|
||||
existingIPs[ip.String()] = struct{}{}
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
ipString := ip.String()
|
||||
_, ok := existingIPs[ipString]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
existingIPs[ipString] = struct{}{}
|
||||
server.IPs = append(server.IPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
cts[city] = server
|
||||
}
|
||||
|
||||
func (cts cityToServer) toServersSlice() (servers []models.Server) {
|
||||
servers = make([]models.Server, 0, len(cts))
|
||||
for _, server := range cts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
20
internal/provider/perfectprivacy/updater/filename.go
Normal file
20
internal/provider/perfectprivacy/updater/filename.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package perfectprivacy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func parseFilename(fileName string) (city string) {
|
||||
const suffix = ".conf"
|
||||
s := strings.TrimSuffix(fileName, suffix)
|
||||
|
||||
for i, r := range s {
|
||||
if unicode.IsDigit(r) {
|
||||
s = s[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
73
internal/provider/perfectprivacy/updater/servers.go
Normal file
73
internal/provider/perfectprivacy/updater/servers.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package perfectprivacy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, unzipper unzip.Unzipper, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
zipURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.perfect-privacy.com",
|
||||
Path: "/downloads/openvpn/get",
|
||||
}
|
||||
values := make(url.Values)
|
||||
values.Set("system", "linux")
|
||||
values.Set("scope", "server")
|
||||
values.Set("filetype", "zip")
|
||||
values.Set("protocol", "udp") // all support both TCP and UDP
|
||||
zipURL.RawQuery = values.Encode()
|
||||
|
||||
contents, err := unzipper.FetchAndExtract(ctx, zipURL.String())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cts := make(cityToServer)
|
||||
|
||||
for fileName, content := range contents {
|
||||
err := addServerFromOvpn(cts, fileName, content)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fileName+": "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(cts) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(cts), minServers)
|
||||
}
|
||||
|
||||
servers = cts.toServersSlice()
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func addServerFromOvpn(cts cityToServer,
|
||||
fileName string, content []byte) (err error) {
|
||||
if !strings.HasSuffix(fileName, ".conf") {
|
||||
return nil // not an OpenVPN file
|
||||
}
|
||||
|
||||
ips, err := openvpn.ExtractIPs(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
city := parseFilename(fileName)
|
||||
|
||||
cts.add(city, ips)
|
||||
|
||||
return nil
|
||||
}
|
||||
13
internal/provider/perfectprivacy/updater/sort.go
Normal file
13
internal/provider/perfectprivacy/updater/sort.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package perfectprivacy
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].City < servers[j].City
|
||||
})
|
||||
}
|
||||
50
internal/provider/privado/updater/hosttoserver.go
Normal file
50
internal/provider/privado/updater/hosttoserver.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package privado
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.Server
|
||||
|
||||
func (hts hostToServer) add(host string) {
|
||||
server, ok := hts[host]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.Hostname = host
|
||||
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][]net.IP) {
|
||||
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
|
||||
}
|
||||
31
internal/provider/privado/updater/location.go
Normal file
31
internal/provider/privado/updater/location.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package privado
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/publicip"
|
||||
)
|
||||
|
||||
func setLocationInfo(ctx context.Context, client *http.Client, servers []models.Server) (err error) {
|
||||
// Get public IP address information
|
||||
ipsToGetInfo := make([]net.IP, 0, len(servers))
|
||||
for _, server := range servers {
|
||||
ipsToGetInfo = append(ipsToGetInfo, server.IPs...)
|
||||
}
|
||||
ipsInfo, err := publicip.MultiInfo(ctx, client, ipsToGetInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range servers {
|
||||
ipInfo := ipsInfo[i]
|
||||
servers[i].Country = ipInfo.Country
|
||||
servers[i].Region = ipInfo.Region
|
||||
servers[i].City = ipInfo.City
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
31
internal/provider/privado/updater/resolve.go
Normal file
31
internal/provider/privado/updater/resolve.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package privado
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 30 * time.Second
|
||||
maxNoNew = 1
|
||||
maxFails = 5
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
76
internal/provider/privado/updater/servers.go
Normal file
76
internal/provider/privado/updater/servers.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Package privado contains code to obtain the server information
|
||||
// for the Privado provider.
|
||||
package privado
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
|
||||
client *http.Client, presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
const url = "https://privado.io/apps/ovpn_configs.zip"
|
||||
contents, err := unzipper.FetchAndExtract(ctx, url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(contents) < minServers {
|
||||
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
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 != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
hts.add(host)
|
||||
}
|
||||
|
||||
if len(hts) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(hts), minServers)
|
||||
}
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
if err := setLocationInfo(ctx, client, servers); err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
22
internal/provider/privado/updater/sort.go
Normal file
22
internal/provider/privado/updater/sort.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package privado
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].Region == servers[j].Region {
|
||||
if servers[i].City == servers[j].City {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Region < servers[j].Region
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
76
internal/provider/privateinternetaccess/updater/api.go
Normal file
76
internal/provider/privateinternetaccess/updater/api.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package privateinternetaccess
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
)
|
||||
|
||||
type apiData struct {
|
||||
Regions []regionData `json:"regions"`
|
||||
}
|
||||
|
||||
type regionData struct {
|
||||
Name string `json:"name"`
|
||||
DNS string `json:"dns"`
|
||||
PortForward bool `json:"port_forward"`
|
||||
Offline bool `json:"offline"`
|
||||
Servers struct {
|
||||
UDP []serverData `json:"ovpnudp"`
|
||||
TCP []serverData `json:"ovpntcp"`
|
||||
} `json:"servers"`
|
||||
}
|
||||
|
||||
type serverData struct {
|
||||
IP net.IP `json:"ip"`
|
||||
CN string `json:"cn"`
|
||||
}
|
||||
|
||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
||||
data apiData, err error) {
|
||||
const url = "https://serverlist.piaservers.net/vpninfo/servers/v5"
|
||||
|
||||
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
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return data, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
||||
response.StatusCode, response.Status)
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
// remove key/signature at the bottom
|
||||
i := bytes.IndexRune(b, '\n')
|
||||
b = b[:i]
|
||||
|
||||
if err := json.Unmarshal(b, &data); err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package privateinternetaccess
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type nameToServer map[string]models.Server
|
||||
|
||||
func (nts nameToServer) add(name, hostname, region string,
|
||||
tcp, udp, portForward bool, ip net.IP) (change bool) {
|
||||
server, ok := nts[name]
|
||||
if !ok {
|
||||
change = true
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.ServerName = name
|
||||
server.Hostname = hostname
|
||||
server.Region = region
|
||||
server.PortForward = portForward
|
||||
}
|
||||
|
||||
if !server.TCP && tcp {
|
||||
change = true
|
||||
server.TCP = tcp
|
||||
}
|
||||
if !server.UDP && udp {
|
||||
change = true
|
||||
server.UDP = udp
|
||||
}
|
||||
|
||||
ipFound := false
|
||||
for _, existingIP := range server.IPs {
|
||||
if ip.Equal(existingIP) {
|
||||
ipFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ipFound {
|
||||
change = true
|
||||
server.IPs = append(server.IPs, ip)
|
||||
}
|
||||
|
||||
nts[name] = server
|
||||
|
||||
return change
|
||||
}
|
||||
|
||||
func (nts nameToServer) toServersSlice() (servers []models.Server) {
|
||||
servers = make([]models.Server, 0, len(nts))
|
||||
for _, server := range nts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
99
internal/provider/privateinternetaccess/updater/servers.go
Normal file
99
internal/provider/privateinternetaccess/updater/servers.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Package pia contains code to obtain the server information
|
||||
// for the Private Internet Access provider.
|
||||
package privateinternetaccess
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, client *http.Client, minServers int) (
|
||||
servers []models.Server, err error) {
|
||||
nts := make(nameToServer)
|
||||
|
||||
noChangeCounter := 0
|
||||
const maxNoChange = 10
|
||||
const betweenDuration = 200 * time.Millisecond
|
||||
const maxDuration = time.Minute
|
||||
|
||||
maxTimer := time.NewTimer(maxDuration)
|
||||
|
||||
for {
|
||||
data, err := fetchAPI(ctx, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
change := addData(data.Regions, nts)
|
||||
|
||||
if !change {
|
||||
noChangeCounter++
|
||||
if noChangeCounter == maxNoChange {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
noChangeCounter = 0
|
||||
}
|
||||
|
||||
timer := time.NewTimer(betweenDuration)
|
||||
maxTimeout := false
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
if !maxTimer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
case <-timer.C:
|
||||
case <-maxTimer.C:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
maxTimeout = true
|
||||
}
|
||||
|
||||
if maxTimeout {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
servers = nts.toServersSlice()
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func addData(regions []regionData, nts nameToServer) (change bool) {
|
||||
for _, region := range regions {
|
||||
for _, server := range region.Servers.UDP {
|
||||
const tcp, udp = false, true
|
||||
if nts.add(server.CN, region.DNS, region.Name, tcp, udp, region.PortForward, server.IP) {
|
||||
change = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, server := range region.Servers.TCP {
|
||||
const tcp, udp = true, false
|
||||
if nts.add(server.CN, region.DNS, region.Name, tcp, udp, region.PortForward, server.IP) {
|
||||
change = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return change
|
||||
}
|
||||
19
internal/provider/privateinternetaccess/updater/sort.go
Normal file
19
internal/provider/privateinternetaccess/updater/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package privateinternetaccess
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Region == servers[j].Region {
|
||||
if servers[i].Hostname == servers[j].Hostname {
|
||||
return servers[i].ServerName < servers[j].ServerName
|
||||
}
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
}
|
||||
14
internal/provider/privatevpn/updater/countries.go
Normal file
14
internal/provider/privatevpn/updater/countries.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package privatevpn
|
||||
|
||||
import "strings"
|
||||
|
||||
func codeToCountry(countryCode string, countryCodes map[string]string) (
|
||||
country string, warning string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
country, ok := countryCodes[countryCode]
|
||||
if !ok {
|
||||
warning = "unknown country code: " + countryCode
|
||||
country = countryCode
|
||||
}
|
||||
return country, warning
|
||||
}
|
||||
57
internal/provider/privatevpn/updater/filename.go
Normal file
57
internal/provider/privatevpn/updater/filename.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package privatevpn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
trailingNumber = regexp.MustCompile(` [0-9]+$`)
|
||||
)
|
||||
|
||||
var (
|
||||
errBadPrefix = errors.New("bad prefix in file name")
|
||||
errBadSuffix = errors.New("bad suffix in file name")
|
||||
errNotEnoughParts = errors.New("not enough parts in file name")
|
||||
)
|
||||
|
||||
func parseFilename(fileName string) (
|
||||
countryCode, city string, err error,
|
||||
) {
|
||||
fileName = strings.ReplaceAll(fileName, " ", "") // remove spaces
|
||||
|
||||
const prefix = "PrivateVPN-"
|
||||
if !strings.HasPrefix(fileName, prefix) {
|
||||
return "", "", fmt.Errorf("%w: %s", errBadPrefix, fileName)
|
||||
}
|
||||
s := strings.TrimPrefix(fileName, prefix)
|
||||
|
||||
const tcpSuffix = "-TUN-443.ovpn"
|
||||
const udpSuffix = "-TUN-1194.ovpn"
|
||||
switch {
|
||||
case strings.HasSuffix(fileName, tcpSuffix):
|
||||
s = strings.TrimSuffix(s, tcpSuffix)
|
||||
case strings.HasSuffix(fileName, udpSuffix):
|
||||
s = strings.TrimSuffix(s, udpSuffix)
|
||||
default:
|
||||
return "", "", fmt.Errorf("%w: %s", errBadSuffix, fileName)
|
||||
}
|
||||
|
||||
s = trailingNumber.ReplaceAllString(s, "")
|
||||
|
||||
parts := strings.Split(s, "-")
|
||||
const minParts = 2
|
||||
if len(parts) < minParts {
|
||||
return "", "", fmt.Errorf("%w: %s", errNotEnoughParts, fileName)
|
||||
}
|
||||
countryCode, city = parts[0], parts[1]
|
||||
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
if countryCode == "co" && strings.HasPrefix(city, "Bogot") {
|
||||
city = "Bogota"
|
||||
}
|
||||
|
||||
return countryCode, city, nil
|
||||
}
|
||||
54
internal/provider/privatevpn/updater/hosttoserver.go
Normal file
54
internal/provider/privatevpn/updater/hosttoserver.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package privatevpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.Server
|
||||
|
||||
// TODO check if server supports TCP and UDP.
|
||||
func (hts hostToServer) add(host, country, city string) {
|
||||
server, ok := hts[host]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.Hostname = host
|
||||
server.Country = country
|
||||
server.City = city
|
||||
server.UDP = true
|
||||
server.TCP = 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][]net.IP) {
|
||||
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
|
||||
}
|
||||
33
internal/provider/privatevpn/updater/resolve.go
Normal file
33
internal/provider/privatevpn/updater/resolve.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package privatevpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 6 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
103
internal/provider/privatevpn/updater/servers.go
Normal file
103
internal/provider/privatevpn/updater/servers.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Package privatevpn contains code to obtain the server information
|
||||
// for the PrivateVPN provider.
|
||||
package privatevpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
|
||||
presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
const url = "https://privatevpn.com/client/PrivateVPN-TUN.zip"
|
||||
contents, err := unzipper.FetchAndExtract(ctx, url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(contents) < minServers {
|
||||
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(contents), minServers)
|
||||
}
|
||||
|
||||
countryCodes := constants.CountryCodes()
|
||||
|
||||
hts := make(hostToServer)
|
||||
noHostnameServers := make([]models.Server, 0, 1) // there is only one for now
|
||||
|
||||
for fileName, content := range contents {
|
||||
if !strings.HasSuffix(fileName, ".ovpn") {
|
||||
continue // not an OpenVPN file
|
||||
}
|
||||
|
||||
countryCode, city, err := parseFilename(fileName)
|
||||
if err != nil {
|
||||
warnings = append(warnings, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
country, warning := codeToCountry(countryCode, countryCodes)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
host, warning, err := openvpn.ExtractHost(content)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err == nil { // found host
|
||||
hts.add(host, country, city)
|
||||
continue
|
||||
}
|
||||
|
||||
ips, extractIPErr := openvpn.ExtractIPs(content)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if extractIPErr != nil {
|
||||
// treat extract host error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
server := models.Server{
|
||||
Country: country,
|
||||
City: city,
|
||||
IPs: ips,
|
||||
UDP: true,
|
||||
TCP: true,
|
||||
}
|
||||
noHostnameServers = append(noHostnameServers, server)
|
||||
}
|
||||
|
||||
if len(noHostnameServers)+len(hts) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers)+len(hts), minServers)
|
||||
}
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
|
||||
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
servers = append(servers, noHostnameServers...)
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
19
internal/provider/privatevpn/updater/sort.go
Normal file
19
internal/provider/privatevpn/updater/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package privatevpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].City == servers[j].City {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
65
internal/provider/protonvpn/updater/api.go
Normal file
65
internal/provider/protonvpn/updater/api.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package protonvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
)
|
||||
|
||||
type apiData struct {
|
||||
LogicalServers []logicalServer
|
||||
}
|
||||
|
||||
type logicalServer struct {
|
||||
Name string
|
||||
ExitCountry string
|
||||
Region *string
|
||||
City *string
|
||||
Servers []physicalServer
|
||||
}
|
||||
|
||||
type physicalServer struct {
|
||||
EntryIP net.IP
|
||||
ExitIP net.IP
|
||||
Domain string
|
||||
Status uint8
|
||||
}
|
||||
|
||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
||||
data apiData, err error) {
|
||||
const url = "https://api.protonmail.ch/vpn/logicals"
|
||||
|
||||
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
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return data, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
||||
response.StatusCode, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return data, fmt.Errorf("failed unmarshaling response body: %w", err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
14
internal/provider/protonvpn/updater/countries.go
Normal file
14
internal/provider/protonvpn/updater/countries.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package protonvpn
|
||||
|
||||
import "strings"
|
||||
|
||||
func codeToCountry(countryCode string, countryCodes map[string]string) (
|
||||
country string, warning string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
country, ok := countryCodes[countryCode]
|
||||
if !ok {
|
||||
warning = "unknown country code: " + countryCode
|
||||
country = countryCode
|
||||
}
|
||||
return country, warning
|
||||
}
|
||||
40
internal/provider/protonvpn/updater/iptoserver.go
Normal file
40
internal/provider/protonvpn/updater/iptoserver.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package protonvpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type ipToServer map[string]models.Server
|
||||
|
||||
func (its ipToServer) add(country, region, city, name, hostname string,
|
||||
entryIP net.IP) {
|
||||
key := entryIP.String()
|
||||
|
||||
server, ok := its[key]
|
||||
if !ok {
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.Country = country
|
||||
server.Region = region
|
||||
server.City = city
|
||||
server.ServerName = name
|
||||
server.Hostname = hostname
|
||||
server.UDP = true
|
||||
server.TCP = true
|
||||
server.IPs = []net.IP{entryIP}
|
||||
} else {
|
||||
server.IPs = append(server.IPs, entryIP)
|
||||
}
|
||||
|
||||
its[key] = server
|
||||
}
|
||||
|
||||
func (its ipToServer) toServersSlice() (servers []models.Server) {
|
||||
servers = make([]models.Server, 0, len(its))
|
||||
for _, server := range its {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
80
internal/provider/protonvpn/updater/servers.go
Normal file
80
internal/provider/protonvpn/updater/servers.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Package protonvpn contains code to obtain the server information
|
||||
// for the ProtonVPN provider.
|
||||
package protonvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, client *http.Client, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
data, err := fetchAPI(ctx, client)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
countryCodes := constants.CountryCodes()
|
||||
|
||||
var count int
|
||||
for _, logicalServer := range data.LogicalServers {
|
||||
count += len(logicalServer.Servers)
|
||||
}
|
||||
|
||||
if count < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, count, minServers)
|
||||
}
|
||||
|
||||
ipToServer := make(ipToServer, count)
|
||||
for _, logicalServer := range data.LogicalServers {
|
||||
region := getStringValue(logicalServer.Region)
|
||||
city := getStringValue(logicalServer.City)
|
||||
name := logicalServer.Name
|
||||
for _, physicalServer := range logicalServer.Servers {
|
||||
if physicalServer.Status == 0 { // disabled so skip server
|
||||
warnings = append(warnings,
|
||||
"ignoring server "+physicalServer.Domain+" with status 0")
|
||||
continue
|
||||
}
|
||||
|
||||
hostname := physicalServer.Domain
|
||||
entryIP := physicalServer.EntryIP
|
||||
|
||||
// Note: for multi-hop use the server name or hostname
|
||||
// instead of the country
|
||||
countryCode := logicalServer.ExitCountry
|
||||
country, warning := codeToCountry(countryCode, countryCodes)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
ipToServer.add(country, region, city, name, hostname, entryIP)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ipToServer) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(ipToServer), minServers)
|
||||
}
|
||||
|
||||
servers = ipToServer.toServersSlice()
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func getStringValue(ptr *string) string {
|
||||
if ptr == nil {
|
||||
return ""
|
||||
}
|
||||
return *ptr
|
||||
}
|
||||
26
internal/provider/protonvpn/updater/sort.go
Normal file
26
internal/provider/protonvpn/updater/sort.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package protonvpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
a, b := servers[i], servers[j]
|
||||
if a.Country == b.Country { //nolint:nestif
|
||||
if a.Region == b.Region {
|
||||
if a.City == b.City {
|
||||
if a.ServerName == b.ServerName {
|
||||
return a.Hostname < b.Hostname
|
||||
}
|
||||
return a.ServerName < b.ServerName
|
||||
}
|
||||
return a.City < b.City
|
||||
}
|
||||
return a.Region < b.Region
|
||||
}
|
||||
return a.Country < b.Country
|
||||
})
|
||||
}
|
||||
54
internal/provider/purevpn/updater/hosttoserver.go
Normal file
54
internal/provider/purevpn/updater/hosttoserver.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package purevpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.Server
|
||||
|
||||
func (hts hostToServer) add(host string, tcp, udp bool) {
|
||||
server, ok := hts[host]
|
||||
if !ok {
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.Hostname = host
|
||||
}
|
||||
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][]net.IP) {
|
||||
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
|
||||
}
|
||||
33
internal/provider/purevpn/updater/resolve.go
Normal file
33
internal/provider/purevpn/updater/resolve.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package purevpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
103
internal/provider/purevpn/updater/servers.go
Normal file
103
internal/provider/purevpn/updater/servers.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Package purevpn contains code to obtain the server information
|
||||
// for the PureVPN provider.
|
||||
package purevpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/publicip"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, client *http.Client,
|
||||
unzipper unzip.Unzipper, presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
const url = "https://d32d3g1fvkpl8y.cloudfront.net/heartbleed/windows/New+OVPN+Files.zip"
|
||||
contents, err := unzipper.FetchAndExtract(ctx, url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(contents) < minServers {
|
||||
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(contents), minServers)
|
||||
}
|
||||
|
||||
hts := make(hostToServer)
|
||||
|
||||
for fileName, content := range contents {
|
||||
if !strings.HasSuffix(fileName, ".ovpn") {
|
||||
continue
|
||||
}
|
||||
|
||||
tcp, udp, err := openvpn.ExtractProto(content)
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
host, warning, err := openvpn.ExtractHost(content)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
hts.add(host, tcp, udp)
|
||||
}
|
||||
|
||||
if len(hts) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(hts), minServers)
|
||||
}
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
// Get public IP address information
|
||||
ipsToGetInfo := make([]net.IP, len(servers))
|
||||
for i := range servers {
|
||||
ipsToGetInfo[i] = servers[i].IPs[0]
|
||||
}
|
||||
ipsInfo, err := publicip.MultiInfo(ctx, client, ipsToGetInfo)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
for i := range servers {
|
||||
servers[i].Country = ipsInfo[i].Country
|
||||
servers[i].Region = ipsInfo[i].Region
|
||||
servers[i].City = ipsInfo[i].City
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
22
internal/provider/purevpn/updater/sort.go
Normal file
22
internal/provider/purevpn/updater/sort.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package purevpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].Region == servers[j].Region {
|
||||
if servers[i].City == servers[j].City {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Region < servers[j].Region
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
76
internal/provider/surfshark/updater/api.go
Normal file
76
internal/provider/surfshark/updater/api.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/surfshark/servers"
|
||||
)
|
||||
|
||||
// Note: no multi-hop and some servers are missing from their API.
|
||||
func addServersFromAPI(ctx context.Context, client *http.Client,
|
||||
hts hostToServer) (err error) {
|
||||
data, err := fetchAPI(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
locationData := servers.LocationData()
|
||||
hostToLocation := hostToLocation(locationData)
|
||||
|
||||
const tcp, udp = true, true
|
||||
for _, serverData := range data {
|
||||
locationData := hostToLocation[serverData.Host] // TODO remove in v4
|
||||
retroLoc := locationData.RetroLoc // empty string if the host has no retro-compatible region
|
||||
hts.add(serverData.Host, serverData.Region, serverData.Country,
|
||||
serverData.Location, retroLoc, tcp, udp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
)
|
||||
|
||||
type serverData struct {
|
||||
Host string `json:"connectionName"`
|
||||
Region string `json:"region"`
|
||||
Country string `json:"country"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
||||
servers []serverData, err error) {
|
||||
const url = "https://my.surfshark.com/vpn/api/v4/server/clusters"
|
||||
|
||||
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("%w: %d %s", ErrHTTPStatusCodeNotOK,
|
||||
response.StatusCode, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&servers); err != nil {
|
||||
return nil, fmt.Errorf("failed unmarshaling response body: %w", err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
169
internal/provider/surfshark/updater/api_test.go
Normal file
169
internal/provider/surfshark/updater/api_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_addServersFromAPI(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
hts hostToServer
|
||||
responseStatus int
|
||||
responseBody io.ReadCloser
|
||||
expected hostToServer
|
||||
err error
|
||||
}{
|
||||
"fetch API error": {
|
||||
responseStatus: http.StatusNoContent,
|
||||
err: errors.New("HTTP status code not OK: 204 No Content"),
|
||||
},
|
||||
"success": {
|
||||
hts: hostToServer{
|
||||
"existinghost": {Hostname: "existinghost"},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`[
|
||||
{"connectionName":"host1","region":"region1","country":"country1","location":"location1"},
|
||||
{"connectionName":"host2","region":"region2","country":"country1","location":"location2"}
|
||||
]`)),
|
||||
expected: map[string]models.Server{
|
||||
"existinghost": {Hostname: "existinghost"},
|
||||
"host1": {
|
||||
VPN: vpn.OpenVPN,
|
||||
Region: "region1",
|
||||
Country: "country1",
|
||||
City: "location1",
|
||||
Hostname: "host1",
|
||||
TCP: true,
|
||||
UDP: true,
|
||||
},
|
||||
"host2": {
|
||||
VPN: vpn.OpenVPN,
|
||||
Region: "region2",
|
||||
Country: "country1",
|
||||
City: "location2",
|
||||
Hostname: "host2",
|
||||
TCP: true,
|
||||
UDP: true,
|
||||
}},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
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://my.surfshark.com/vpn/api/v4/server/clusters")
|
||||
return &http.Response{
|
||||
StatusCode: testCase.responseStatus,
|
||||
Status: http.StatusText(testCase.responseStatus),
|
||||
Body: testCase.responseBody,
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
err := addServersFromAPI(ctx, client, testCase.hts)
|
||||
|
||||
assert.Equal(t, testCase.expected, testCase.hts)
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fetchAPI(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
responseStatus int
|
||||
responseBody io.ReadCloser
|
||||
data []serverData
|
||||
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("failed unmarshaling response body: EOF"),
|
||||
},
|
||||
"no server": {
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`[]`)),
|
||||
data: []serverData{},
|
||||
},
|
||||
"success": {
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`[
|
||||
{"connectionName":"host1","region":"region1","country":"country1","location":"location1"},
|
||||
{"connectionName":"host2","region":"region2","country":"country1","location":"location2"}
|
||||
]`)),
|
||||
data: []serverData{
|
||||
{
|
||||
Region: "region1",
|
||||
Country: "country1",
|
||||
Location: "location1",
|
||||
Host: "host1",
|
||||
},
|
||||
{
|
||||
Region: "region2",
|
||||
Country: "country1",
|
||||
Location: "location2",
|
||||
Host: "host2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
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://my.surfshark.com/vpn/api/v4/server/clusters")
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
58
internal/provider/surfshark/updater/hosttoserver.go
Normal file
58
internal/provider/surfshark/updater/hosttoserver.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"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, country, city, retroLoc string, tcp, udp bool) {
|
||||
server, ok := hts[host]
|
||||
if !ok {
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.Hostname = host
|
||||
server.Region = region
|
||||
server.Country = country
|
||||
server.City = city
|
||||
server.RetroLoc = retroLoc
|
||||
}
|
||||
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)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]net.IP) {
|
||||
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
|
||||
}
|
||||
31
internal/provider/surfshark/updater/location.go
Normal file
31
internal/provider/surfshark/updater/location.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/surfshark/servers"
|
||||
)
|
||||
|
||||
var (
|
||||
errHostnameNotFound = errors.New("hostname not found in hostname to location mapping")
|
||||
)
|
||||
|
||||
func getHostInformation(host string, hostnameToLocation map[string]servers.ServerLocation) (
|
||||
data servers.ServerLocation, err error) {
|
||||
locationData, ok := hostnameToLocation[host]
|
||||
if !ok {
|
||||
return locationData, fmt.Errorf("%w: %s", errHostnameNotFound, host)
|
||||
}
|
||||
|
||||
return locationData, nil
|
||||
}
|
||||
|
||||
func hostToLocation(locationData []servers.ServerLocation) (
|
||||
hostToLocation map[string]servers.ServerLocation) {
|
||||
hostToLocation = make(map[string]servers.ServerLocation, len(locationData))
|
||||
for _, data := range locationData {
|
||||
hostToLocation[data.Hostname] = data
|
||||
}
|
||||
return hostToLocation
|
||||
}
|
||||
21
internal/provider/surfshark/updater/remaining.go
Normal file
21
internal/provider/surfshark/updater/remaining.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/provider/surfshark/servers"
|
||||
)
|
||||
|
||||
// getRemainingServers finds extra servers not found in the API or in the ZIP file.
|
||||
func getRemainingServers(hts hostToServer) {
|
||||
locationData := servers.LocationData()
|
||||
hostnameToLocationLeft := hostToLocation(locationData)
|
||||
for _, hostnameDone := range hts.toHostsSlice() {
|
||||
delete(hostnameToLocationLeft, hostnameDone)
|
||||
}
|
||||
|
||||
for hostname, locationData := range hostnameToLocationLeft {
|
||||
// we assume the server supports TCP and UDP
|
||||
const tcp, udp = true, true
|
||||
hts.add(hostname, locationData.Region, locationData.Country,
|
||||
locationData.City, locationData.RetroLoc, tcp, udp)
|
||||
}
|
||||
}
|
||||
33
internal/provider/surfshark/updater/resolve.go
Normal file
33
internal/provider/surfshark/updater/resolve.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
57
internal/provider/surfshark/updater/resolve_test.go
Normal file
57
internal/provider/surfshark/updater/resolve_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_resolveHosts(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
ctx := context.Background()
|
||||
presolver := mock_resolver.NewMockParallel(ctrl)
|
||||
hosts := []string{"host1", "host2"}
|
||||
const minServers = 10
|
||||
|
||||
expectedHostToIPs := map[string][]net.IP{
|
||||
"host1": {{1, 2, 3, 4}},
|
||||
"host2": {{2, 3, 4, 5}},
|
||||
}
|
||||
expectedWarnings := []string{"warning1", "warning2"}
|
||||
expectedErr := errors.New("dummy")
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
expectedSettings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
presolver.EXPECT().Resolve(ctx, hosts, expectedSettings).
|
||||
Return(expectedHostToIPs, expectedWarnings, expectedErr)
|
||||
|
||||
hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
assert.Equal(t, expectedHostToIPs, hostToIPs)
|
||||
assert.Equal(t, expectedWarnings, warnings)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
}
|
||||
9
internal/provider/surfshark/updater/roundtrip_test.go
Normal file
9
internal/provider/surfshark/updater/roundtrip_test.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package surfshark
|
||||
|
||||
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)
|
||||
}
|
||||
56
internal/provider/surfshark/updater/servers.go
Normal file
56
internal/provider/surfshark/updater/servers.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Package surfshark contains code to obtain the server information
|
||||
// for the Surshark provider.
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
)
|
||||
|
||||
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
|
||||
client *http.Client, presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
hts := make(hostToServer)
|
||||
|
||||
err = addServersFromAPI(ctx, client, hts)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot fetch server information from API: %w", err)
|
||||
}
|
||||
|
||||
warnings, err = addOpenVPNServersFromZip(ctx, unzipper, hts)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot get OpenVPN ZIP file: %w", err)
|
||||
}
|
||||
|
||||
getRemainingServers(hts)
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
22
internal/provider/surfshark/updater/sort.go
Normal file
22
internal/provider/surfshark/updater/sort.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Region == servers[j].Region {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].City == servers[j].City {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
}
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
}
|
||||
42
internal/provider/surfshark/updater/sort_test.go
Normal file
42
internal/provider/surfshark/updater/sort_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_sortServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
initialServers []models.Server
|
||||
sortedServers []models.Server
|
||||
}{
|
||||
"no server": {},
|
||||
"sorted servers": {
|
||||
initialServers: []models.Server{
|
||||
{Region: "Z", Country: "B", City: "A", Hostname: "A"},
|
||||
{Country: "B", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "A", Hostname: "B"},
|
||||
{Country: "A", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "B", Hostname: "A"},
|
||||
},
|
||||
sortedServers: []models.Server{
|
||||
{Country: "A", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "A", Hostname: "B"},
|
||||
{Country: "A", City: "B", Hostname: "A"},
|
||||
{Country: "B", City: "A", Hostname: "A"},
|
||||
{Region: "Z", Country: "B", City: "A", Hostname: "A"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sortServers(testCase.initialServers)
|
||||
assert.Equal(t, testCase.sortedServers, testCase.initialServers)
|
||||
})
|
||||
}
|
||||
}
|
||||
72
internal/provider/surfshark/updater/zip.go
Normal file
72
internal/provider/surfshark/updater/zip.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/surfshark/servers"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
)
|
||||
|
||||
func addOpenVPNServersFromZip(ctx context.Context,
|
||||
unzipper unzip.Unzipper, hts hostToServer) (
|
||||
warnings []string, err error) {
|
||||
const url = "https://my.surfshark.com/vpn/api/v1/server/configurations"
|
||||
contents, err := unzipper.FetchAndExtract(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostnamesDone := hts.toHostsSlice()
|
||||
hostnamesDoneSet := make(map[string]struct{}, len(hostnamesDone))
|
||||
for _, hostname := range hostnamesDone {
|
||||
hostnamesDoneSet[hostname] = struct{}{}
|
||||
}
|
||||
|
||||
locationData := servers.LocationData()
|
||||
hostToLocation := hostToLocation(locationData)
|
||||
|
||||
for fileName, content := range contents {
|
||||
if !strings.HasSuffix(fileName, ".ovpn") {
|
||||
continue // not an OpenVPN file
|
||||
}
|
||||
|
||||
host, warning, err := openvpn.ExtractHost(content)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
_, ok := hostnamesDoneSet[host]
|
||||
if ok {
|
||||
continue // already done in API
|
||||
}
|
||||
|
||||
tcp, udp, err := openvpn.ExtractProto(content)
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := getHostInformation(host, hostToLocation)
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error()
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
hts.add(host, data.Region, data.Country, data.City,
|
||||
data.RetroLoc, tcp, udp)
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
35
internal/provider/torguard/updater/filename.go
Normal file
35
internal/provider/torguard/updater/filename.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package torguard
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
)
|
||||
|
||||
func parseFilename(fileName string, titleCaser cases.Caser) (country, city string) {
|
||||
const prefix = "TorGuard."
|
||||
const suffix = ".ovpn"
|
||||
s := strings.TrimPrefix(fileName, prefix)
|
||||
s = strings.TrimSuffix(s, suffix)
|
||||
|
||||
switch {
|
||||
case strings.Count(s, ".") == 1 && !strings.HasPrefix(s, "USA"):
|
||||
parts := strings.Split(s, ".")
|
||||
country = parts[0]
|
||||
city = parts[1]
|
||||
|
||||
case strings.HasPrefix(s, "USA"):
|
||||
country = "USA"
|
||||
s = strings.TrimPrefix(s, "USA-")
|
||||
s = strings.ReplaceAll(s, "-", " ")
|
||||
s = strings.ReplaceAll(s, ".", " ")
|
||||
s = strings.ToLower(s)
|
||||
s = titleCaser.String(s)
|
||||
city = s
|
||||
|
||||
default:
|
||||
country = s
|
||||
}
|
||||
|
||||
return country, city
|
||||
}
|
||||
61
internal/provider/torguard/updater/hosttoserver.go
Normal file
61
internal/provider/torguard/updater/hosttoserver.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package torguard
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"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, ips []net.IP) {
|
||||
server, ok := hts[host]
|
||||
if !ok {
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.Hostname = host
|
||||
server.Country = country
|
||||
server.City = city
|
||||
server.IPs = ips // used if DNS resolution fails downstream
|
||||
} else {
|
||||
server.IPs = append(server.IPs, ips...)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]net.IP) {
|
||||
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
|
||||
}
|
||||
33
internal/provider/torguard/updater/resolve.go
Normal file
33
internal/provider/torguard/updater/resolve.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package torguard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
106
internal/provider/torguard/updater/servers.go
Normal file
106
internal/provider/torguard/updater/servers.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Package torguard contains code to obtain the server information
|
||||
// for the Torguard provider.
|
||||
package torguard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
|
||||
presolver resolver.Parallel, minServers int) (
|
||||
servers []models.Server, warnings []string, err error) {
|
||||
const tcpURL = "https://torguard.net/downloads/OpenVPN-TCP-Linux.zip"
|
||||
tcpContents, err := unzipper.FetchAndExtract(ctx, tcpURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
const udpURL = "https://torguard.net/downloads/OpenVPN-UDP-Linux.zip"
|
||||
udpContents, err := unzipper.FetchAndExtract(ctx, udpURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
hts := make(hostToServer)
|
||||
titleCaser := cases.Title(language.English)
|
||||
|
||||
for fileName, content := range tcpContents {
|
||||
const tcp, udp = true, false
|
||||
newWarnings := addServerFromOvpn(fileName, content, hts, tcp, udp, titleCaser)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
}
|
||||
|
||||
for fileName, content := range udpContents {
|
||||
const tcp, udp = false, true
|
||||
newWarnings := addServerFromOvpn(fileName, content, hts, tcp, udp, titleCaser)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
}
|
||||
|
||||
if len(hts) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(hts), minServers)
|
||||
}
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func addServerFromOvpn(fileName string, content []byte,
|
||||
hts hostToServer, tcp, udp bool, titleCaser cases.Caser) (warnings []string) {
|
||||
if !strings.HasSuffix(fileName, ".ovpn") {
|
||||
return nil // not an OpenVPN file
|
||||
}
|
||||
|
||||
country, city := parseFilename(fileName, titleCaser)
|
||||
|
||||
host, warning, err := openvpn.ExtractHost(content)
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
return warnings
|
||||
}
|
||||
|
||||
ips, err := openvpn.ExtractIPs(content)
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
return warnings
|
||||
}
|
||||
|
||||
hts.add(host, country, city, tcp, udp, ips)
|
||||
return warnings
|
||||
}
|
||||
19
internal/provider/torguard/updater/sort.go
Normal file
19
internal/provider/torguard/updater/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package torguard
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.Server) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].City == servers[j].City {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
162
internal/provider/vpnunlimited/updater/constants.go
Normal file
162
internal/provider/vpnunlimited/updater/constants.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package vpnunlimited
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func getHostToServer() (hts hostToServer, warnings []string) {
|
||||
shortHTS := map[string]models.Server{
|
||||
"ae": {},
|
||||
"ar": {},
|
||||
"at": {},
|
||||
"au-syd": {
|
||||
City: "Sydney",
|
||||
},
|
||||
"ba": {},
|
||||
"be": {},
|
||||
"bg": {},
|
||||
"br": {},
|
||||
"by": {},
|
||||
"ca-tr": {
|
||||
City: "Toronto",
|
||||
},
|
||||
"ca-vn": {
|
||||
City: "Vancouver",
|
||||
},
|
||||
"ca": {},
|
||||
"ch": {},
|
||||
"cr": {},
|
||||
"cy": {},
|
||||
"cz": {},
|
||||
"de-dus": {
|
||||
City: "Düsseldorf",
|
||||
},
|
||||
"de": {},
|
||||
"dk": {},
|
||||
"ee": {},
|
||||
"es": {},
|
||||
"fi": {},
|
||||
"fr-rbx": {
|
||||
City: "Roubaix",
|
||||
},
|
||||
"fr": {},
|
||||
"gr": {},
|
||||
"hr": {},
|
||||
"hu": {},
|
||||
"ie-dub": {
|
||||
City: "Dublin",
|
||||
},
|
||||
"il": {},
|
||||
"im": {},
|
||||
"in-ka": {
|
||||
City: "Karnataka",
|
||||
},
|
||||
"in": {},
|
||||
"is": {},
|
||||
"it-mil": {
|
||||
City: "Milan",
|
||||
},
|
||||
"jp": {},
|
||||
"kr": {},
|
||||
"lt": {},
|
||||
"lu": {},
|
||||
"lv": {},
|
||||
"ly": {},
|
||||
"md": {},
|
||||
"mx": {},
|
||||
"mys": {},
|
||||
"nl": {},
|
||||
"no": {},
|
||||
"nz": {},
|
||||
"om": {},
|
||||
"pl": {},
|
||||
"pt": {},
|
||||
"ro": {},
|
||||
"rs": {},
|
||||
"se": {},
|
||||
"sg-free": {
|
||||
Free: true,
|
||||
},
|
||||
"sg": {},
|
||||
"si": {},
|
||||
"sk": {},
|
||||
"th": {},
|
||||
"tr": {},
|
||||
"uk-cv": {
|
||||
City: "London",
|
||||
},
|
||||
"uk-lon": {
|
||||
City: "London",
|
||||
},
|
||||
"uk": {},
|
||||
"us-chi": {
|
||||
City: "Chicago",
|
||||
},
|
||||
"us-dal": {
|
||||
City: "Dallas",
|
||||
},
|
||||
"us-den": {
|
||||
City: "Denver",
|
||||
},
|
||||
"us-hou": {
|
||||
City: "Houston",
|
||||
},
|
||||
"us-la": {
|
||||
City: "Los Angeles",
|
||||
},
|
||||
"us-lv": {
|
||||
City: "Las Vegas",
|
||||
},
|
||||
"us-mia": {
|
||||
City: "Miami",
|
||||
},
|
||||
"us-ny-free": {
|
||||
City: "New York",
|
||||
Free: true,
|
||||
},
|
||||
"us-ny": {
|
||||
City: "New York",
|
||||
},
|
||||
"us-sea": {
|
||||
City: "Seattle",
|
||||
},
|
||||
"us-sf": {
|
||||
City: "San Francisco",
|
||||
},
|
||||
"us-sl": {
|
||||
City: "Saint Louis",
|
||||
},
|
||||
"us-slc": {
|
||||
City: "Salt Lake City",
|
||||
},
|
||||
"us-stream": {
|
||||
Stream: true,
|
||||
},
|
||||
"us": {},
|
||||
"vn": {},
|
||||
"za": {},
|
||||
}
|
||||
|
||||
hts = make(hostToServer, len(shortHTS))
|
||||
|
||||
countryCodesMap := constants.CountryCodes()
|
||||
for shortHost, server := range shortHTS {
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.UDP = true
|
||||
server.Hostname = shortHost + ".vpnunlimitedapp.com"
|
||||
countryCode := strings.Split(shortHost, "-")[0]
|
||||
country, ok := countryCodesMap[countryCode]
|
||||
if !ok {
|
||||
warnings = append(warnings, "country code not found: "+countryCode)
|
||||
continue
|
||||
}
|
||||
server.Country = country
|
||||
hts[server.Hostname] = server
|
||||
}
|
||||
|
||||
return hts, warnings
|
||||
}
|
||||
38
internal/provider/vpnunlimited/updater/hosttoserver.go
Normal file
38
internal/provider/vpnunlimited/updater/hosttoserver.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package vpnunlimited
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.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][]net.IP) {
|
||||
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
|
||||
}
|
||||
33
internal/provider/vpnunlimited/updater/resolve.go
Normal file
33
internal/provider/vpnunlimited/updater/resolve.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package vpnunlimited
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user