Files
gluetun/internal/provider/expressvpn/updater/candidates.go
Quentin McGaw c5b9ee89c6 expressvpn wip
2026-08-06 03:22:06 +00:00

147 lines
4.5 KiB
Go

package updater
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/models"
)
// generateCandidateHostnames generates multiple hostname candidates for a given location
// by trying different slug variations, number suffixes, and country aliases.
func generateCandidateHostnames(server models.Server) (hostnames []string, err error) {
country := strings.ToLower(server.Country)
city := strings.ToLower(server.City)
if strings.Contains(country, " (via ") {
destinationEndIndex := strings.Index(country, " (via ")
destination := strings.TrimSpace(country[:destinationEndIndex])
destination = strings.ReplaceAll(destination, " ", "")
sourceEndIndex := strings.Index(country[destinationEndIndex:], ")")
if sourceEndIndex == -1 {
return nil, fmt.Errorf("invalid location format, missing closing parenthesis for source: %q", country)
}
source := strings.TrimSpace(country[destinationEndIndex+len(" (via ") : destinationEndIndex+sourceEndIndex])
source = strings.ReplaceAll(source, " ", "")
sourceAliases := countryNameToAliases(source)
sources := make([]string, 0, 1+len(sourceAliases))
sources = append(sources, source)
sources = append(sources, sourceAliases...)
destinationAliases := countryNameToAliases(destination)
destinations := make([]string, 0, 1+len(destinationAliases))
destinations = append(destinations, destination)
destinations = append(destinations, destinationAliases...)
slugs := make([]string, 0, (1+len(sourceAliases))*(1+len(destinationAliases)))
for _, destination := range destinations {
for _, source := range sources {
slugs = append(slugs, destination+"-"+source)
}
}
if city != "" {
return nil, fmt.Errorf("city %q should be empty for multi-hop country: %s", city, country)
}
return makeNumberedCandidates(slugs, server.Number), nil
}
countrySlug := strings.ReplaceAll(strings.TrimSpace(country), " ", "")
countrySlugAliases := countryNameToAliases(countrySlug)
countrySlugs := make([]string, 0, 1+len(countrySlugAliases))
countrySlugs = append(countrySlugs, countrySlug)
countrySlugs = append(countrySlugs, countrySlugAliases...)
var citySlugs []string
if city != "" {
if strings.Contains(city, " ") {
citySlugs = append(citySlugs, strings.ReplaceAll(city, " ", ""))
citySlugs = append(citySlugs, strings.ReplaceAll(city, " ", "-"))
} else {
citySlugs = append(citySlugs, city)
}
}
var slugs []string
if len(citySlugs) == 0 {
slugs = countrySlugs
} else {
slugs = make([]string, 0, len(countrySlugs)*len(citySlugs))
for _, countrySlug := range countrySlugs {
for _, citySlug := range citySlugs {
slugs = append(slugs, countrySlug+"-"+citySlug)
}
}
}
return makeNumberedCandidates(slugs, server.Number), nil
}
func makeNumberedCandidates(baseSlugs []string, serverNumber uint16) (candidates []string) {
numbersToTry := []uint16{1, 2, 3, 4, 5}
if serverNumber > 0 {
numbersToTry = []uint16{serverNumber}
}
const candidateVariationsPerNumber = 2 // slugN and slug-N
candidateVariationsPerBase := 1 + len(numbersToTry)*candidateVariationsPerNumber // base + (slugN and slug-N for each number)
candidates = make([]string, 0, len(baseSlugs)*candidateVariationsPerBase)
for _, base := range baseSlugs {
candidates = append(candidates, base+hostnameSuffix)
for _, number := range numbersToTry {
candidates = append(candidates,
base+fmt.Sprintf("%d-ca-version-2.expressnetw.com", number),
base+fmt.Sprintf("-%d-ca-version-2.expressnetw.com", number))
}
}
return candidates
}
// hostnameSuffix is appended to all ExpressVPN hostnames.
const hostnameSuffix = "-ca-version-2.expressnetw.com"
// countryNameToAliases maps common country names to alternative names used in hostnames.
func countryNameToAliases(country string) (aliases []string) {
switch country {
case "usa":
return []string{"us"}
case "uk":
return []string{"gb"}
case "netherlands":
return []string{"nl"}
case "switzerland":
return []string{"ch"}
case "japan":
return []string{"jp"}
case "sweden":
return []string{"se"}
case "norway":
return []string{"no"}
case "denmark":
return []string{"dk"}
case "finland":
return []string{"fi"}
case "portugal":
return []string{"pt"}
case "spain":
return []string{"es"}
case "germany":
return []string{"de"}
case "italy":
return []string{"it"}
case "australia":
return []string{"au"}
case "singapore":
return []string{"sg"}
case "taiwan":
return []string{"tw"}
case "southkorea":
return []string{"kr"}
case "northmacedonia":
return []string{"macedonia"}
default:
return nil
}
}