diff --git a/internal/constants/servers_test.go b/internal/constants/servers_test.go index 971dfb53..b4910df9 100644 --- a/internal/constants/servers_test.go +++ b/internal/constants/servers_test.go @@ -62,7 +62,7 @@ func Test_versions(t *testing.T) { "Purevpn": { model: models.PurevpnServer{}, version: allServers.Purevpn.Version, - digest: "cc1a2219", + digest: "ada45379", }, "Surfshark": { model: models.SurfsharkServer{}, diff --git a/internal/models/server.go b/internal/models/server.go index e6f6a7ef..0857fd41 100644 --- a/internal/models/server.go +++ b/internal/models/server.go @@ -96,15 +96,15 @@ func (s *NordvpnServer) String() string { } type PurevpnServer struct { - Region string `json:"region"` Country string `json:"country"` + Region string `json:"region"` City string `json:"city"` IPs []net.IP `json:"ips"` } func (s *PurevpnServer) String() string { - return fmt.Sprintf("{Region: %q, Country: %q, City: %q, IPs: %s}", - s.Region, s.Country, s.City, goStringifyIPs(s.IPs)) + return fmt.Sprintf("{Country: %q, Region: %q, City: %q, IPs: %s}", + s.Country, s.Region, s.City, goStringifyIPs(s.IPs)) } type PrivadoServer struct { diff --git a/internal/updater/ips.go b/internal/updater/ips.go index 85589a69..9062ef8f 100644 --- a/internal/updater/ips.go +++ b/internal/updater/ips.go @@ -2,8 +2,16 @@ package updater import ( "bytes" + "context" + "encoding/json" + "errors" + "fmt" "net" + "net/http" "sort" + "strings" + + "github.com/qdm12/golibs/network" ) func uniqueSortedIPs(ips []net.IP) []net.IP { @@ -25,3 +33,35 @@ func uniqueSortedIPs(ips []net.IP) []net.IP { }) return ips } + +var errBadHTTPStatus = errors.New("bad HTTP status received") + +type ipInfoData struct { + Region string `json:"region"` + Country string `json:"country"` + City string `json:"city"` +} + +func getIPInfo(ctx context.Context, client network.Client, ip net.IP) (country, region, city string, err error) { + const baseURL = "https://ipinfo.io/" + url := baseURL + ip.String() + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", "", "", err + } + b, status, err := client.Do(request) + if err != nil { + return "", "", "", err + } else if status != http.StatusOK { + return "", "", "", fmt.Errorf("%w: %d", errBadHTTPStatus, status) + } + var data ipInfoData + if err := json.Unmarshal(b, &data); err != nil { + return "", "", "", err + } + country, ok := getCountryCodes()[strings.ToLower(data.Country)] + if !ok { + country = data.Country + } + return country, data.Region, data.City, nil +} diff --git a/internal/updater/purevpn.go b/internal/updater/purevpn.go index c64b4347..45cd23e9 100644 --- a/internal/updater/purevpn.go +++ b/internal/updater/purevpn.go @@ -2,9 +2,7 @@ package updater import ( "context" - "encoding/json" "fmt" - "net/http" "sort" "strings" @@ -32,74 +30,72 @@ func (u *updater) updatePurevpn(ctx context.Context) (err error) { func findPurevpnServers(ctx context.Context, client network.Client, lookupIP lookupIPFunc) ( servers []models.PurevpnServer, warnings []string, err error) { - const url = "https://support.purevpn.com/vpn-servers" - bytes, status, err := client.Get(ctx, url) + const zipURL = "https://s3-us-west-1.amazonaws.com/heartbleed/windows/New+OVPN+Files.zip" + contents, err := fetchAndExtractFiles(ctx, client, zipURL) if err != nil { return nil, nil, err } - if status != http.StatusOK { - return nil, nil, fmt.Errorf("HTTP status code %d", status) - } - const jsonPrefix = "" - s := string(bytes) - jsonPrefixIndex := strings.Index(s, jsonPrefix) - if jsonPrefixIndex == -1 { - return nil, nil, fmt.Errorf("cannot find %q in html", jsonPrefix) - } - s = s[jsonPrefixIndex+len(jsonPrefix):] - endIndex := strings.Index(s, jsonSuffix) - if endIndex == -1 { - return nil, nil, fmt.Errorf("cannot find %q after %q in html", jsonSuffix, jsonPrefix) - } - s = s[:endIndex] - var data []struct { - Region string `json:"region_name"` - Country string `json:"country_name"` - City string `json:"city_name"` - TCP string `json:"tcp"` - UDP string `json:"udp"` - } - if err := json.Unmarshal([]byte(s), &data); err != nil { - return nil, nil, err - } - sort.Slice(data, func(i, j int) bool { - if data[i].Region == data[j].Region { - if data[i].Country == data[j].Country { - return data[i].City < data[j].City - } - return data[i].Country < data[j].Country - } - return data[i].Region < data[j].Region - }) - for _, jsonServer := range data { + uniqueServers := map[string]models.PurevpnServer{} + for fileName, content := range contents { if err := ctx.Err(); err != nil { return nil, warnings, err } - if jsonServer.UDP == "" && jsonServer.TCP == "" { - warnings = append(warnings, fmt.Sprintf("server %s %s %s does not support TCP and UDP for openvpn", - jsonServer.Region, jsonServer.Country, jsonServer.City)) - continue + if strings.HasSuffix(fileName, "-tcp.ovpn") { + continue // only parse UDP files } - if jsonServer.UDP == "" || jsonServer.TCP == "" { - warnings = append(warnings, fmt.Sprintf("server %s %s %s does not support TCP or UDP for openvpn", - jsonServer.Region, jsonServer.Country, jsonServer.City)) - continue + host, warning, err := extractHostFromOVPN(content) + if len(warning) > 0 { + warnings = append(warnings, warning) + } + if err != nil { + return nil, warnings, fmt.Errorf("%w in %q", err, fileName) } - host := jsonServer.UDP const repetition = 5 IPs, err := resolveRepeat(ctx, lookupIP, host, repetition) - if err != nil { - warnings = append(warnings, err.Error()) + switch { + case err != nil: + return nil, warnings, err + case len(IPs) == 0: + warning := fmt.Sprintf("no IP address found for host %q", host) + warnings = append(warnings, warning) continue } - servers = append(servers, models.PurevpnServer{ - Region: jsonServer.Region, - Country: jsonServer.Country, - City: jsonServer.City, - IPs: IPs, - }) + country, region, city, err := getIPInfo(ctx, client, IPs[0]) + if err != nil { + return nil, warnings, err + } + key := country + region + city + server, ok := uniqueServers[key] + if ok { + server.IPs = append(server.IPs, IPs...) + } else { + server = models.PurevpnServer{ + Country: country, + Region: region, + City: city, + IPs: IPs, + } + } + uniqueServers[key] = server } + + servers = make([]models.PurevpnServer, len(uniqueServers)) + i := 0 + for _, server := range uniqueServers { + servers[i] = server + i++ + } + + sort.Slice(servers, func(i, j int) bool { + if servers[i].Country == servers[j].Country { + if servers[i].Region == servers[j].Region { + return servers[i].City < servers[j].City + } + return servers[i].Region < servers[j].Region + } + return servers[i].Country < servers[j].Country + }) + return servers, warnings, nil }