Maintenance: refactor servers updater code
- Require at least 80% of number of servers now to pass - Each provider is in its own package with a common structure - Unzip package with unzipper interface - Openvpn package with extraction and download functions
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
package updater
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
ErrUnmarshalResponseBody = errors.New("cannot unmarshal response body")
|
||||
ErrUpdateServerInformation = errors.New("failed updating server information")
|
||||
)
|
||||
@@ -1,161 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func (u *updater) updateFastestvpn(ctx context.Context) (err error) {
|
||||
servers, warnings, err := findFastestvpnServersFromZip(ctx, u.client, u.presolver)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("FastestVPN: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update FastestVPN servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyFastestVPNServers(servers))
|
||||
}
|
||||
u.servers.Fastestvpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Fastestvpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func findFastestvpnServersFromZip(ctx context.Context, client *http.Client, presolver resolver.Parallel) (
|
||||
servers []models.FastestvpnServer, warnings []string, err error) {
|
||||
const zipURL = "https://support.fastestvpn.com/download/openvpn-tcp-udp-config-files"
|
||||
contents, err := fetchAndExtractFiles(ctx, client, zipURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
trailNumberExp := regexp.MustCompile(`[0-9]+$`)
|
||||
|
||||
type Data struct {
|
||||
TCP bool
|
||||
UDP bool
|
||||
Country string
|
||||
}
|
||||
hostToData := make(map[string]Data)
|
||||
|
||||
for fileName, content := range contents {
|
||||
const (
|
||||
tcpSuffix = "-TCP.ovpn"
|
||||
udpSuffix = "-UDP.ovpn"
|
||||
)
|
||||
var tcp, udp bool
|
||||
var suffix string
|
||||
switch {
|
||||
case strings.HasSuffix(fileName, tcpSuffix):
|
||||
suffix = tcpSuffix
|
||||
tcp = true
|
||||
case strings.HasSuffix(fileName, udpSuffix):
|
||||
suffix = udpSuffix
|
||||
udp = true
|
||||
default:
|
||||
warning := `filename "` + fileName + `" does not have a protocol suffix`
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
countryWithNumber := strings.TrimSuffix(fileName, suffix)
|
||||
number := trailNumberExp.FindString(countryWithNumber)
|
||||
country := countryWithNumber[:len(countryWithNumber)-len(number)]
|
||||
|
||||
host, warning, err := extractHostFromOVPN(content)
|
||||
if len(warning) > 0 {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warnings = append(warnings, err.Error()+" in "+fileName)
|
||||
continue
|
||||
}
|
||||
|
||||
data := hostToData[host]
|
||||
data.Country = country
|
||||
if tcp {
|
||||
data.TCP = true
|
||||
}
|
||||
if udp {
|
||||
data.UDP = true
|
||||
}
|
||||
hostToData[host] = data
|
||||
}
|
||||
|
||||
hosts := make([]string, len(hostToData))
|
||||
i := 0
|
||||
for host := range hostToData {
|
||||
hosts[i] = host
|
||||
i++
|
||||
}
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxNoNew = 1
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: time.Second,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
hostToIPs, newWarnings, err := presolver.Resolve(ctx, hosts, settings)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
for host, IPs := range hostToIPs {
|
||||
if len(IPs) == 0 {
|
||||
warning := fmt.Sprintf("no IP address found for host %q", host)
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
data := hostToData[host]
|
||||
|
||||
server := models.FastestvpnServer{
|
||||
Hostname: host,
|
||||
TCP: data.TCP,
|
||||
UDP: data.UDP,
|
||||
Country: data.Country,
|
||||
IPs: IPs,
|
||||
}
|
||||
servers = append(servers, 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
|
||||
})
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func stringifyFastestVPNServers(servers []models.FastestvpnServer) (s string) {
|
||||
s = "func FastestvpnServers() []models.FastestvpnServer {\n"
|
||||
s += " return []models.FastestvpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func (u *updater) updateHideMyAss(ctx context.Context) (err error) {
|
||||
servers, warnings, err := findHideMyAssServers(ctx, u.client, u.presolver)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("HideMyAss: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: HideMyAss: %s", ErrUpdateServerInformation, err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyHideMyAssServers(servers))
|
||||
}
|
||||
u.servers.HideMyAss.Timestamp = u.timeNow().Unix()
|
||||
u.servers.HideMyAss.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func findHideMyAssServers(ctx context.Context, client *http.Client, presolver resolver.Parallel) (
|
||||
servers []models.HideMyAssServer, warnings []string, err error) {
|
||||
TCPhostToURL, err := findHideMyAssHostToURLForProto(ctx, client, "TCP")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
UDPhostToURL, err := findHideMyAssHostToURLForProto(ctx, client, "UDP")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
uniqueHosts := make(map[string]struct{}, len(TCPhostToURL))
|
||||
for host := range TCPhostToURL {
|
||||
uniqueHosts[host] = struct{}{}
|
||||
}
|
||||
for host := range UDPhostToURL {
|
||||
uniqueHosts[host] = struct{}{}
|
||||
}
|
||||
|
||||
hosts := make([]string, len(uniqueHosts))
|
||||
i := 0
|
||||
for host := range uniqueHosts {
|
||||
hosts[i] = host
|
||||
i++
|
||||
}
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 15 * time.Second
|
||||
betweenDuration = 2 * time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
hostToIPs, warnings, err := presolver.Resolve(ctx, hosts, settings)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
servers = make([]models.HideMyAssServer, 0, len(hostToIPs))
|
||||
for host, IPs := range hostToIPs {
|
||||
tcpURL, tcp := TCPhostToURL[host]
|
||||
udpURL, udp := UDPhostToURL[host]
|
||||
|
||||
var url, protocol string
|
||||
if tcp {
|
||||
url = tcpURL
|
||||
protocol = "TCP"
|
||||
} else if udp {
|
||||
url = udpURL
|
||||
protocol = "UDP"
|
||||
}
|
||||
country, region, city := parseHideMyAssURL(url, protocol)
|
||||
|
||||
server := models.HideMyAssServer{
|
||||
Country: country,
|
||||
Region: region,
|
||||
City: city,
|
||||
Hostname: host,
|
||||
IPs: IPs,
|
||||
TCP: tcp,
|
||||
UDP: udp,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].Country+servers[i].Region+servers[i].City+servers[i].Hostname <
|
||||
servers[j].Country+servers[j].Region+servers[j].City+servers[j].Hostname
|
||||
})
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func findHideMyAssHostToURLForProto(ctx context.Context, client *http.Client, protocol string) (
|
||||
hostToURL map[string]string, err error) {
|
||||
indexURL := "https://vpn.hidemyass.com/vpn-config/" + strings.ToUpper(protocol) + "/"
|
||||
|
||||
urls, err := fetchHideMyAssHTTPIndex(ctx, client, indexURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fetchMultiOvpnFiles(ctx, client, urls)
|
||||
}
|
||||
|
||||
func parseHideMyAssURL(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]
|
||||
}
|
||||
|
||||
return camelCaseToWords(country), camelCaseToWords(region), camelCaseToWords(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
|
||||
}
|
||||
|
||||
var hideMyAssIndexRegex = regexp.MustCompile(`<a[ ]+href=".+\.ovpn">.+\.ovpn</a>`)
|
||||
|
||||
func fetchHideMyAssHTTPIndex(ctx context.Context, client *http.Client, indexURL string) (urls []string, err error) {
|
||||
htmlCode, err := fetchFile(ctx, client, indexURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(indexURL, "/") {
|
||||
indexURL += "/"
|
||||
}
|
||||
|
||||
lines := strings.Split(string(htmlCode), "\n")
|
||||
for _, line := range lines {
|
||||
found := hideMyAssIndexRegex.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]
|
||||
url := indexURL + filename
|
||||
if !strings.HasSuffix(url, ".ovpn") {
|
||||
continue
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
|
||||
func fetchMultiOvpnFiles(ctx context.Context, client *http.Client, urls []string) (
|
||||
hostToURL map[string]string, err error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
hostToURL = make(map[string]string, len(urls))
|
||||
|
||||
type Result struct {
|
||||
url string
|
||||
host string
|
||||
}
|
||||
results := make(chan Result)
|
||||
errors := make(chan error)
|
||||
for _, url := range urls {
|
||||
go func(url string) {
|
||||
host, err := fetchOvpnFile(ctx, client, url)
|
||||
if err != nil {
|
||||
errors <- fmt.Errorf("%w: for %s", err, url)
|
||||
return
|
||||
}
|
||||
results <- Result{
|
||||
url: url,
|
||||
host: host,
|
||||
}
|
||||
}(url)
|
||||
}
|
||||
|
||||
for range urls {
|
||||
select {
|
||||
case newErr := <-errors:
|
||||
if err == nil { // only assign to the first error
|
||||
err = newErr
|
||||
cancel() // stop other operations, this will trigger other errors we ignore
|
||||
}
|
||||
case result := <-results:
|
||||
hostToURL[result.host] = result.url
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hostToURL, nil
|
||||
}
|
||||
|
||||
func fetchOvpnFile(ctx context.Context, client *http.Client, url string) (hostname string, err error) {
|
||||
b, err := fetchFile(ctx, client, url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
const rejectIP = true
|
||||
const rejectDomain = false
|
||||
hosts := extractRemoteHostsFromOpenvpn(b, rejectIP, rejectDomain)
|
||||
if len(hosts) == 0 {
|
||||
return "", errRemoteHostNotFound
|
||||
}
|
||||
|
||||
return hosts[0], nil
|
||||
}
|
||||
|
||||
func fetchFile(ctx context.Context, client *http.Client, url string) (b []byte, err error) {
|
||||
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()
|
||||
|
||||
return io.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
func stringifyHideMyAssServers(servers []models.HideMyAssServer) (s string) {
|
||||
s = "func HideMyAssServers() []models.HideMyAssServer {\n"
|
||||
s += " return []models.HideMyAssServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func (u *updater) updateMullvad(ctx context.Context) (err error) {
|
||||
servers, err := findMullvadServers(ctx, u.client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update Mullvad servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyMullvadServers(servers))
|
||||
}
|
||||
u.servers.Mullvad.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Mullvad.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func findMullvadServers(ctx context.Context, client *http.Client) (servers []models.MullvadServer, err error) {
|
||||
const url = "https://api.mullvad.net/www/relays/openvpn/"
|
||||
|
||||
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 for %s", ErrHTTPStatusCodeNotOK, response.Status, url)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var data []struct {
|
||||
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"`
|
||||
}
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serversByKey := map[string]models.MullvadServer{}
|
||||
for _, jsonServer := range data {
|
||||
if !jsonServer.Active {
|
||||
continue
|
||||
}
|
||||
ipv4 := net.ParseIP(jsonServer.IPv4)
|
||||
ipv6 := net.ParseIP(jsonServer.IPv6)
|
||||
if ipv4 == nil || ipv4.To4() == nil {
|
||||
return nil, fmt.Errorf("cannot parse ipv4 address %q", jsonServer.IPv4)
|
||||
} else if ipv6 == nil || ipv6.To4() != nil {
|
||||
return nil, fmt.Errorf("cannot parse ipv6 address %q", jsonServer.IPv6)
|
||||
}
|
||||
key := fmt.Sprintf("%s%s%t%s", jsonServer.Country, jsonServer.City, jsonServer.Owned, jsonServer.Provider)
|
||||
if server, ok := serversByKey[key]; ok {
|
||||
server.IPs = append(server.IPs, ipv4)
|
||||
server.IPsV6 = append(server.IPsV6, ipv6)
|
||||
serversByKey[key] = server
|
||||
} else {
|
||||
serversByKey[key] = models.MullvadServer{
|
||||
IPs: []net.IP{ipv4},
|
||||
IPsV6: []net.IP{ipv6},
|
||||
Country: jsonServer.Country,
|
||||
City: strings.ReplaceAll(jsonServer.City, ",", ""),
|
||||
ISP: jsonServer.Provider,
|
||||
Owned: jsonServer.Owned,
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, server := range serversByKey {
|
||||
server.IPs = uniqueSortedIPs(server.IPs)
|
||||
server.IPsV6 = uniqueSortedIPs(server.IPsV6)
|
||||
servers = append(servers, server)
|
||||
}
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].Country+servers[i].City+servers[i].ISP < servers[j].Country+servers[j].City+servers[j].ISP
|
||||
})
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func stringifyMullvadServers(servers []models.MullvadServer) (s string) {
|
||||
s = "func MullvadServers() []models.MullvadServer {\n"
|
||||
s += " return []models.MullvadServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func (u *updater) updateNordvpn(ctx context.Context) (err error) {
|
||||
servers, warnings, err := findNordvpnServers(ctx, u.client)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Nordvpn: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update Nordvpn servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyNordvpnServers(servers))
|
||||
}
|
||||
u.servers.Nordvpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Nordvpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoIDInServerName = errors.New("no ID in server name")
|
||||
ErrInvalidIDInServerName = errors.New("invalid ID in server name")
|
||||
)
|
||||
|
||||
func findNordvpnServers(ctx context.Context, client *http.Client) (
|
||||
servers []models.NordvpnServer, warnings []string, err error) {
|
||||
const url = "https://nordvpn.com/api/server"
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("%w: %s for %s", ErrHTTPStatusCodeNotOK, response.Status, url)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var data []struct {
|
||||
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"`
|
||||
}
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sort.Slice(data, func(i, j int) bool {
|
||||
if data[i].Country == data[j].Country {
|
||||
return data[i].Name < data[j].Name
|
||||
}
|
||||
return data[i].Country < data[j].Country
|
||||
})
|
||||
|
||||
for _, jsonServer := range data {
|
||||
if !jsonServer.Features.TCP && !jsonServer.Features.UDP {
|
||||
warnings = append(warnings, fmt.Sprintf("server %q does not support TCP and UDP for openvpn", jsonServer.Name))
|
||||
continue
|
||||
}
|
||||
ip := net.ParseIP(jsonServer.IPAddress)
|
||||
if ip == nil || ip.To4() == nil {
|
||||
return nil, nil,
|
||||
fmt.Errorf("IP address %q is not a valid IPv4 address for server %q",
|
||||
jsonServer.IPAddress, jsonServer.Name)
|
||||
}
|
||||
i := strings.IndexRune(jsonServer.Name, '#')
|
||||
if i < 0 {
|
||||
return nil, nil, fmt.Errorf("%w: %s", ErrNoIDInServerName, jsonServer.Name)
|
||||
}
|
||||
idString := jsonServer.Name[i+1:]
|
||||
idUint64, err := strconv.ParseUint(idString, 10, 16)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%w: %s", ErrInvalidIDInServerName, jsonServer.Name)
|
||||
}
|
||||
server := models.NordvpnServer{
|
||||
Region: jsonServer.Country,
|
||||
Number: uint16(idUint64),
|
||||
IP: ip,
|
||||
TCP: jsonServer.Features.TCP,
|
||||
UDP: jsonServer.Features.UDP,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func stringifyNordvpnServers(servers []models.NordvpnServer) (s string) {
|
||||
s = "func NordvpnServers() []models.NordvpnServer {\n"
|
||||
s += " return []models.NordvpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errRemoteHostNotFound = errors.New("remote host not found")
|
||||
)
|
||||
|
||||
func extractHostFromOVPN(b []byte) (host, warning string, err error) {
|
||||
const (
|
||||
rejectIP = true
|
||||
rejectDomain = false
|
||||
)
|
||||
hosts := extractRemoteHostsFromOpenvpn(b, rejectIP, rejectDomain)
|
||||
if len(hosts) == 0 {
|
||||
return "", "", errRemoteHostNotFound
|
||||
} else if len(hosts) > 1 {
|
||||
warning = fmt.Sprintf(
|
||||
"only using the first host %q and discarding %d other hosts",
|
||||
hosts[0], len(hosts)-1)
|
||||
}
|
||||
return hosts[0], warning, nil
|
||||
}
|
||||
|
||||
func extractRemoteHostsFromOpenvpn(content []byte,
|
||||
rejectIP, rejectDomain bool) (hosts []string) {
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
if !strings.HasPrefix(line, "remote ") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 1 || len(fields[1]) == 0 {
|
||||
continue
|
||||
}
|
||||
host := fields[1]
|
||||
parsedIP := net.ParseIP(host)
|
||||
if (rejectIP && parsedIP != nil) ||
|
||||
(rejectDomain && parsedIP == nil) {
|
||||
continue
|
||||
}
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
66
internal/updater/openvpn/extract.go
Normal file
66
internal/updater/openvpn/extract.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoRemoteHost = errors.New("remote host not found")
|
||||
ErrNoRemoteIP = errors.New("remote IP not found")
|
||||
)
|
||||
|
||||
func ExtractHost(b []byte) (host, warning string, err error) {
|
||||
const (
|
||||
rejectIP = true
|
||||
rejectDomain = false
|
||||
)
|
||||
hosts := extractRemoteHosts(b, rejectIP, rejectDomain)
|
||||
if len(hosts) == 0 {
|
||||
return "", "", ErrNoRemoteHost
|
||||
} else if len(hosts) > 1 {
|
||||
warning = fmt.Sprintf(
|
||||
"only using the first host %q and discarding %d other hosts",
|
||||
hosts[0], len(hosts)-1)
|
||||
}
|
||||
return hosts[0], warning, nil
|
||||
}
|
||||
|
||||
func ExtractIP(b []byte) (ip net.IP, warning string, err error) {
|
||||
const (
|
||||
rejectIP = false
|
||||
rejectDomain = true
|
||||
)
|
||||
ips := extractRemoteHosts(b, rejectIP, rejectDomain)
|
||||
if len(ips) == 0 {
|
||||
return nil, "", ErrNoRemoteIP
|
||||
} else if len(ips) > 1 {
|
||||
warning = fmt.Sprintf(
|
||||
"only using the first IP address %s and discarding %d other hosts",
|
||||
ips[0], len(ips)-1)
|
||||
}
|
||||
return net.ParseIP(ips[0]), warning, nil
|
||||
}
|
||||
|
||||
func extractRemoteHosts(content []byte, rejectIP, rejectDomain bool) (hosts []string) {
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
if !strings.HasPrefix(line, "remote ") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 1 || len(fields[1]) == 0 {
|
||||
continue
|
||||
}
|
||||
host := fields[1]
|
||||
parsedIP := net.ParseIP(host)
|
||||
if (rejectIP && parsedIP != nil) ||
|
||||
(rejectDomain && parsedIP == nil) {
|
||||
continue
|
||||
}
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
41
internal/updater/openvpn/fetch.go
Normal file
41
internal/updater/openvpn/fetch.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func FetchFile(ctx context.Context, client *http.Client, url string) (
|
||||
host string, err error) {
|
||||
b, err := fetchData(ctx, client, url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
const rejectIP = true
|
||||
const rejectDomain = false
|
||||
hosts := extractRemoteHosts(b, rejectIP, rejectDomain)
|
||||
if len(hosts) == 0 {
|
||||
return "", fmt.Errorf("%w for url %s", ErrNoRemoteHost, url)
|
||||
}
|
||||
|
||||
return hosts[0], nil
|
||||
}
|
||||
|
||||
func fetchData(ctx context.Context, client *http.Client, url string) (
|
||||
b []byte, err error) {
|
||||
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()
|
||||
|
||||
return io.ReadAll(response.Body)
|
||||
}
|
||||
59
internal/updater/openvpn/multifetch.go
Normal file
59
internal/updater/openvpn/multifetch.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FetchMultiFiles fetches multiple Openvpn files in parallel and
|
||||
// parses them to extract each of their host. A mapping from host to
|
||||
// URL is returned.
|
||||
func FetchMultiFiles(ctx context.Context, client *http.Client, urls []string) (
|
||||
hostToURL map[string]string, err error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
hostToURL = make(map[string]string, len(urls))
|
||||
|
||||
type Result struct {
|
||||
url string
|
||||
host string
|
||||
}
|
||||
|
||||
results := make(chan Result)
|
||||
defer close(results)
|
||||
errors := make(chan error)
|
||||
defer close(errors)
|
||||
|
||||
for _, url := range urls {
|
||||
go func(url string) {
|
||||
host, err := FetchFile(ctx, client, url)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
results <- Result{
|
||||
url: url,
|
||||
host: host,
|
||||
}
|
||||
}(url)
|
||||
}
|
||||
|
||||
for range urls {
|
||||
select {
|
||||
case newErr := <-errors:
|
||||
if err == nil { // only assign to the first error
|
||||
err = newErr
|
||||
cancel() // stop other operations, this will trigger other errors we ignore
|
||||
}
|
||||
case result := <-results:
|
||||
hostToURL[result.host] = result.url
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hostToURL, nil
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func (u *updater) updatePIA(ctx context.Context) (err error) {
|
||||
const url = "https://serverlist.piaservers.net/vpninfo/servers/v5"
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := u.client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%w: %s for %s", ErrHTTPStatusCodeNotOK, response.Status, url)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove key/signature at the bottom
|
||||
i := bytes.IndexRune(b, '\n')
|
||||
b = b[:i]
|
||||
|
||||
var data struct {
|
||||
Regions []struct {
|
||||
Name string `json:"name"`
|
||||
PortForward bool `json:"port_forward"`
|
||||
Servers struct {
|
||||
UDP []struct {
|
||||
IP net.IP `json:"ip"`
|
||||
CN string `json:"cn"`
|
||||
} `json:"ovpnudp"`
|
||||
TCP []struct {
|
||||
IP net.IP `json:"ip"`
|
||||
CN string `json:"cn"`
|
||||
} `json:"ovpntcp"`
|
||||
} `json:"servers"`
|
||||
} `json:"regions"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Deduplicate servers with the same IP address
|
||||
type NetProtocols struct {
|
||||
tcp, udp bool
|
||||
}
|
||||
ipToProtocols := make(map[string]NetProtocols)
|
||||
|
||||
for _, region := range data.Regions {
|
||||
for _, udpServer := range region.Servers.UDP {
|
||||
protocols := ipToProtocols[udpServer.IP.String()]
|
||||
protocols.udp = true
|
||||
ipToProtocols[udpServer.IP.String()] = protocols
|
||||
}
|
||||
for _, tcpServer := range region.Servers.TCP {
|
||||
protocols := ipToProtocols[tcpServer.IP.String()]
|
||||
protocols.tcp = true
|
||||
ipToProtocols[tcpServer.IP.String()] = protocols
|
||||
}
|
||||
}
|
||||
|
||||
servers := make([]models.PIAServer, 0, len(ipToProtocols)) // set the capacity, not the length of the slice
|
||||
for _, region := range data.Regions {
|
||||
for _, udpServer := range region.Servers.UDP {
|
||||
protocols, ok := ipToProtocols[udpServer.IP.String()]
|
||||
if !ok { // already added that IP for a server
|
||||
continue
|
||||
}
|
||||
server := models.PIAServer{
|
||||
Region: region.Name,
|
||||
ServerName: udpServer.CN,
|
||||
TCP: protocols.tcp,
|
||||
UDP: protocols.udp,
|
||||
PortForward: region.PortForward,
|
||||
IP: udpServer.IP,
|
||||
}
|
||||
delete(ipToProtocols, udpServer.IP.String())
|
||||
servers = append(servers, server)
|
||||
}
|
||||
for _, tcpServer := range region.Servers.TCP {
|
||||
protocols, ok := ipToProtocols[tcpServer.IP.String()]
|
||||
if !ok { // already added that IP for a server
|
||||
continue
|
||||
}
|
||||
server := models.PIAServer{
|
||||
Region: region.Name,
|
||||
ServerName: tcpServer.CN,
|
||||
TCP: protocols.tcp,
|
||||
UDP: protocols.udp,
|
||||
PortForward: region.PortForward,
|
||||
IP: tcpServer.IP,
|
||||
}
|
||||
delete(ipToProtocols, tcpServer.IP.String())
|
||||
servers = append(servers, server)
|
||||
}
|
||||
}
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Region == servers[j].Region {
|
||||
return servers[i].ServerName < servers[j].ServerName
|
||||
}
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyPIAServers(servers))
|
||||
}
|
||||
u.servers.Pia.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Pia.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringifyPIAServers(servers []models.PIAServer) (s string) {
|
||||
s = "func PIAServers() []models.PIAServer {\n"
|
||||
s += " return []models.PIAServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func (u *updater) updatePrivado(ctx context.Context) (err error) {
|
||||
servers, warnings, err := findPrivadoServersFromZip(ctx, u.client, u.presolver)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Privado: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update Privado servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyPrivadoServers(servers))
|
||||
}
|
||||
u.servers.Privado.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Privado.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func findPrivadoServersFromZip(ctx context.Context, client *http.Client, presolver resolver.Parallel) (
|
||||
servers []models.PrivadoServer, warnings []string, err error) {
|
||||
const zipURL = "https://privado.io/apps/ovpn_configs.zip"
|
||||
contents, err := fetchAndExtractFiles(ctx, client, zipURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
hosts := make([]string, 0, len(contents))
|
||||
for fileName, content := range contents {
|
||||
hostname, 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)
|
||||
}
|
||||
hosts = append(hosts, hostname)
|
||||
}
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 3 * time.Second
|
||||
maxNoNew = 1
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
hostToIPs, newWarnings, err := presolver.Resolve(ctx, hosts, settings)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
for hostname, IPs := range hostToIPs {
|
||||
switch len(IPs) {
|
||||
case 0:
|
||||
warning := fmt.Sprintf("no IP address found for host %q", hostname)
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
case 1:
|
||||
default:
|
||||
warning := fmt.Sprintf("more than one IP address found for host %q", hostname)
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
server := models.PrivadoServer{
|
||||
Hostname: hostname,
|
||||
IP: IPs[0],
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
})
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func stringifyPrivadoServers(servers []models.PrivadoServer) (s string) {
|
||||
s = "func PrivadoServers() []models.PrivadoServer {\n"
|
||||
s += " return []models.PrivadoServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func (u *updater) updatePrivatevpn(ctx context.Context) (err error) {
|
||||
servers, warnings, err := findPrivatevpnServersFromZip(ctx, u.client, u.presolver)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Privatevpn: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update Privatevpn servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyPrivatevpnServers(servers))
|
||||
}
|
||||
u.servers.Privatevpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Privatevpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func findPrivatevpnServersFromZip(ctx context.Context, client *http.Client, presolver resolver.Parallel) (
|
||||
servers []models.PrivatevpnServer, warnings []string, err error) {
|
||||
// Note: all servers do both TCP and UDP
|
||||
const zipURL = "https://privatevpn.com/client/PrivateVPN-TUN.zip"
|
||||
|
||||
contents, err := fetchAndExtractFiles(ctx, client, zipURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
trailingNumber := regexp.MustCompile(` [0-9]+$`)
|
||||
countryCodes := constants.CountryCodes()
|
||||
|
||||
uniqueServers := map[string]models.PrivatevpnServer{} // key is the hostname
|
||||
|
||||
for fileName, content := range contents {
|
||||
const prefix = "PrivateVPN-"
|
||||
const suffix = "-TUN-443.ovpn"
|
||||
|
||||
if !strings.HasSuffix(fileName, suffix) {
|
||||
continue // only process TCP servers as they're the same
|
||||
}
|
||||
|
||||
var server models.PrivatevpnServer
|
||||
|
||||
s := strings.TrimPrefix(fileName, prefix)
|
||||
s = strings.TrimSuffix(s, suffix)
|
||||
s = trailingNumber.ReplaceAllString(s, "")
|
||||
|
||||
parts := strings.Split(s, "-")
|
||||
var countryCode string
|
||||
countryCode, server.City = parts[0], parts[1]
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
var countryCodeOK bool
|
||||
server.Country, countryCodeOK = countryCodes[countryCode]
|
||||
if !countryCodeOK {
|
||||
warnings = append(warnings, "unknown country code: "+countryCode)
|
||||
server.Country = countryCode
|
||||
}
|
||||
|
||||
var warning string
|
||||
server.Hostname, warning, err = extractHostFromOVPN(content)
|
||||
if len(warning) > 0 {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
if len(warning) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
uniqueServers[server.Hostname] = server
|
||||
}
|
||||
|
||||
hostnames := make([]string, len(uniqueServers))
|
||||
i := 0
|
||||
for hostname := range uniqueServers {
|
||||
hostnames[i] = hostname
|
||||
i++
|
||||
}
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 6 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
hostToIPs, newWarnings, err := presolver.Resolve(ctx, hostnames, settings)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
for hostname, server := range uniqueServers {
|
||||
ips := hostToIPs[hostname]
|
||||
if len(ips) == 0 {
|
||||
continue
|
||||
}
|
||||
server.IPs = ips
|
||||
servers = append(servers, 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
|
||||
})
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func stringifyPrivatevpnServers(servers []models.PrivatevpnServer) (s string) {
|
||||
s = "func PrivatevpnServers() []models.PrivatevpnServer {\n"
|
||||
s += " return []models.PrivatevpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func (u *updater) updateProtonvpn(ctx context.Context) (err error) {
|
||||
servers, warnings, err := findProtonvpnServers(ctx, u.client)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Protonvpn: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update Protonvpn servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyProtonvpnServers(servers))
|
||||
}
|
||||
u.servers.Protonvpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Protonvpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func findProtonvpnServers(ctx context.Context, client *http.Client) (
|
||||
servers []models.ProtonvpnServer, warnings []string, err error) {
|
||||
const url = "https://api.protonmail.ch/vpn/logicals"
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("%w: %s for %s", ErrHTTPStatusCodeNotOK, response.Status, url)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var data struct {
|
||||
LogicalServers []struct {
|
||||
Name string
|
||||
ExitCountry string
|
||||
Region *string
|
||||
City *string
|
||||
Servers []struct {
|
||||
EntryIP net.IP
|
||||
ExitIP net.IP
|
||||
Domain string
|
||||
Status uint8
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
countryCodesMapping := constants.CountryCodes()
|
||||
for _, logicalServer := range data.LogicalServers {
|
||||
for _, physicalServer := range logicalServer.Servers {
|
||||
if physicalServer.Status == 0 {
|
||||
warnings = append(warnings, "ignoring server "+physicalServer.Domain+" as its status is 0")
|
||||
continue
|
||||
}
|
||||
|
||||
countryCode := strings.ToLower(logicalServer.ExitCountry)
|
||||
country, ok := countryCodesMapping[countryCode]
|
||||
if !ok {
|
||||
warnings = append(warnings, "country not found for country code "+countryCode)
|
||||
country = logicalServer.ExitCountry
|
||||
}
|
||||
|
||||
server := models.ProtonvpnServer{
|
||||
// Note: for multi-hop use the server name or hostname instead of the country
|
||||
Country: country,
|
||||
Region: getStringValue(logicalServer.Region),
|
||||
City: getStringValue(logicalServer.City),
|
||||
Name: logicalServer.Name,
|
||||
Hostname: physicalServer.Domain,
|
||||
EntryIP: physicalServer.EntryIP,
|
||||
ExitIP: physicalServer.ExitIP,
|
||||
}
|
||||
servers = append(servers, 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.Name == b.Name {
|
||||
return a.Hostname < b.Hostname
|
||||
}
|
||||
return a.Name < b.Name
|
||||
}
|
||||
return a.City < b.City
|
||||
}
|
||||
return a.Region < b.Region
|
||||
}
|
||||
return a.Country < b.Country
|
||||
})
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func getStringValue(ptr *string) string {
|
||||
if ptr == nil {
|
||||
return ""
|
||||
}
|
||||
return *ptr
|
||||
}
|
||||
|
||||
func stringifyProtonvpnServers(servers []models.ProtonvpnServer) (s string) {
|
||||
s = "func ProtonvpnServers() []models.ProtonvpnServer {\n"
|
||||
s += " return []models.ProtonvpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
279
internal/updater/providers.go
Normal file
279
internal/updater/providers.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/cyberghost"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/fastestvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/hidemyass"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/mullvad"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/nordvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/pia"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/privado"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/privatevpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/protonvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/purevpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/surfshark"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/torguard"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/vyprvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/windscribe"
|
||||
)
|
||||
|
||||
func (u *updater) updateCyberghost(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Cyberghost.Servers))
|
||||
servers, err := cyberghost.GetServers(ctx, u.presolver, minServers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(cyberghost.Stringify(servers))
|
||||
}
|
||||
u.servers.Cyberghost.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Cyberghost.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateFastestvpn(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Fastestvpn.Servers))
|
||||
servers, warnings, err := fastestvpn.GetServers(
|
||||
ctx, u.unzipper, u.presolver, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("FastestVPN: " + warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(fastestvpn.Stringify(servers))
|
||||
}
|
||||
u.servers.Fastestvpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Fastestvpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateHideMyAss(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.HideMyAss.Servers))
|
||||
servers, warnings, err := hidemyass.GetServers(
|
||||
ctx, u.client, u.presolver, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("HideMyAss: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(hidemyass.Stringify(servers))
|
||||
}
|
||||
u.servers.HideMyAss.Timestamp = u.timeNow().Unix()
|
||||
u.servers.HideMyAss.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateMullvad(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Mullvad.Servers))
|
||||
servers, err := mullvad.GetServers(ctx, u.client, minServers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(mullvad.Stringify(servers))
|
||||
}
|
||||
u.servers.Mullvad.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Mullvad.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateNordvpn(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Nordvpn.Servers))
|
||||
servers, warnings, err := nordvpn.GetServers(ctx, u.client, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("NordVPN: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(nordvpn.Stringify(servers))
|
||||
}
|
||||
u.servers.Nordvpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Nordvpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updatePIA(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Pia.Servers))
|
||||
servers, err := pia.GetServers(ctx, u.client, minServers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(pia.Stringify(servers))
|
||||
}
|
||||
u.servers.Pia.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Pia.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updatePrivado(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Privado.Servers))
|
||||
servers, warnings, err := privado.GetServers(
|
||||
ctx, u.unzipper, u.presolver, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Privado: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(privado.Stringify(servers))
|
||||
}
|
||||
u.servers.Privado.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Privado.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updatePrivatevpn(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Privatevpn.Servers))
|
||||
servers, warnings, err := privatevpn.GetServers(
|
||||
ctx, u.unzipper, u.presolver, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("PrivateVPN: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(privatevpn.Stringify(servers))
|
||||
}
|
||||
u.servers.Privatevpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Privatevpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateProtonvpn(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Privatevpn.Servers))
|
||||
servers, warnings, err := protonvpn.GetServers(ctx, u.client, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("ProtonVPN: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(protonvpn.Stringify(servers))
|
||||
}
|
||||
u.servers.Protonvpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Protonvpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updatePurevpn(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Purevpn.Servers))
|
||||
servers, warnings, err := purevpn.GetServers(
|
||||
ctx, u.client, u.unzipper, u.presolver, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("PureVPN: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update Purevpn servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(purevpn.Stringify(servers))
|
||||
}
|
||||
u.servers.Purevpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Purevpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateSurfshark(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Surfshark.Servers))
|
||||
servers, warnings, err := surfshark.GetServers(
|
||||
ctx, u.unzipper, u.presolver, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Surfshark: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(surfshark.Stringify(servers))
|
||||
}
|
||||
u.servers.Surfshark.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Surfshark.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateTorguard(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Torguard.Servers))
|
||||
servers, warnings, err := torguard.GetServers(ctx, u.unzipper, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Torguard: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(torguard.Stringify(servers))
|
||||
}
|
||||
u.servers.Torguard.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Torguard.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateVyprvpn(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Vyprvpn.Servers))
|
||||
servers, warnings, err := vyprvpn.GetServers(
|
||||
ctx, u.unzipper, u.presolver, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("VyprVPN: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(vyprvpn.Stringify(servers))
|
||||
}
|
||||
u.servers.Vyprvpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Vyprvpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateWindscribe(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Windscribe.Servers))
|
||||
servers, err := windscribe.GetServers(ctx, u.client, minServers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(windscribe.Stringify(servers))
|
||||
}
|
||||
u.servers.Windscribe.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Windscribe.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func getMinServers(existingServers int) (minServers int) {
|
||||
const minRatio = 0.8
|
||||
return int(minRatio * float64(existingServers))
|
||||
}
|
||||
@@ -1,117 +1,6 @@
|
||||
package updater
|
||||
package cyberghost
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func (u *updater) updateCyberghost(ctx context.Context) (err error) {
|
||||
servers, err := findCyberghostServers(ctx, u.presolver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyCyberghostServers(servers))
|
||||
}
|
||||
u.servers.Cyberghost.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Cyberghost.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func findCyberghostServers(ctx context.Context, presolver resolver.Parallel) (
|
||||
servers []models.CyberghostServer, err error) {
|
||||
groups := getCyberghostGroups()
|
||||
allCountryCodes := constants.CountryCodes()
|
||||
cyberghostCountryCodes := getCyberghostSubdomainToRegion()
|
||||
possibleCountryCodes := mergeCountryCodes(cyberghostCountryCodes, allCountryCodes)
|
||||
|
||||
// key is the host
|
||||
possibleServers := make(map[string]models.CyberghostServer, len(groups)*len(possibleCountryCodes))
|
||||
possibleHosts := make([]string, 0, len(groups)*len(possibleCountryCodes))
|
||||
for groupID, groupName := range groups {
|
||||
for countryCode, region := range possibleCountryCodes {
|
||||
const domain = "cg-dialup.net"
|
||||
possibleHost := groupID + "-" + countryCode + "." + domain
|
||||
possibleHosts = append(possibleHosts, possibleHost)
|
||||
possibleServer := models.CyberghostServer{
|
||||
Region: region,
|
||||
Group: groupName,
|
||||
}
|
||||
possibleServers[possibleHost] = possibleServer
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
maxFailRatio = 1
|
||||
minFound = 100
|
||||
maxDuration = 10 * time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 1
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minFound,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: time.Second,
|
||||
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
|
||||
}
|
||||
|
||||
// Set IPs for servers found
|
||||
for host, IPs := range hostToIPs {
|
||||
server := possibleServers[host]
|
||||
server.IPs = IPs
|
||||
possibleServers[host] = server
|
||||
}
|
||||
|
||||
// Remove servers with no IPs (aka not found)
|
||||
for host, server := range possibleServers {
|
||||
if len(server.IPs) == 0 {
|
||||
delete(possibleServers, host)
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten possibleServers to a slice
|
||||
servers = make([]models.CyberghostServer, 0, len(possibleServers))
|
||||
for _, server := range possibleServers {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
//nolint:goconst
|
||||
func stringifyCyberghostServers(servers []models.CyberghostServer) (s string) {
|
||||
s = "func CyberghostServers() []models.CyberghostServer {\n"
|
||||
s += " return []models.CyberghostServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
|
||||
func getCyberghostGroups() map[string]string {
|
||||
func getGroups() map[string]string {
|
||||
return map[string]string{
|
||||
"87-1": "Premium UDP Europe",
|
||||
"94-1": "Premium UDP USA",
|
||||
@@ -124,21 +13,7 @@ func getCyberghostGroups() map[string]string {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func getCyberghostSubdomainToRegion() map[string]string {
|
||||
func getSubdomainToRegion() map[string]string {
|
||||
return map[string]string{
|
||||
"af": "Afghanistan",
|
||||
"ax": "Aland Islands",
|
||||
15
internal/updater/providers/cyberghost/countries.go
Normal file
15
internal/updater/providers/cyberghost/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
|
||||
}
|
||||
65
internal/updater/providers/cyberghost/hosttoserver.go
Normal file
65
internal/updater/providers/cyberghost/hosttoserver.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package cyberghost
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.CyberghostServer
|
||||
|
||||
func getPossibleServers() (possibleServers hostToServer) {
|
||||
groups := getGroups()
|
||||
|
||||
cyberghostCountryCodes := getSubdomainToRegion()
|
||||
allCountryCodes := constants.CountryCodes()
|
||||
possibleCountryCodes := mergeCountryCodes(cyberghostCountryCodes, allCountryCodes)
|
||||
|
||||
n := len(groups) * len(possibleCountryCodes)
|
||||
|
||||
possibleServers = make(hostToServer, n) // key is the host
|
||||
|
||||
for groupID, groupName := range groups {
|
||||
for countryCode, region := range possibleCountryCodes {
|
||||
const domain = "cg-dialup.net"
|
||||
possibleHost := groupID + "-" + countryCode + "." + domain
|
||||
possibleServer := models.CyberghostServer{
|
||||
Region: region,
|
||||
Group: groupName,
|
||||
}
|
||||
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.CyberghostServer) {
|
||||
servers = make([]models.CyberghostServer, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
42
internal/updater/providers/cyberghost/resolve.go
Normal file
42
internal/updater/providers/cyberghost/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 = 10 * time.Second
|
||||
betweenDuration = 500 * time.Millisecond
|
||||
maxNoNew = 2
|
||||
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/updater/providers/cyberghost/servers.go
Normal file
28
internal/updater/providers/cyberghost/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.CyberghostServer, 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/updater/providers/cyberghost/sort.go
Normal file
16
internal/updater/providers/cyberghost/sort.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package cyberghost
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.CyberghostServer) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Region == servers[j].Region {
|
||||
return servers[i].Group < servers[j].Group
|
||||
}
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
}
|
||||
15
internal/updater/providers/cyberghost/string.go
Normal file
15
internal/updater/providers/cyberghost/string.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package cyberghost
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
// Stringify converts servers to code string format.
|
||||
func Stringify(servers []models.CyberghostServer) (s string) {
|
||||
s = "func CyberghostServers() []models.CyberghostServer {\n"
|
||||
s += " return []models.CyberghostServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
39
internal/updater/providers/fastestvpn/filename.go
Normal file
39
internal/updater/providers/fastestvpn/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
|
||||
}
|
||||
53
internal/updater/providers/fastestvpn/hosttoserver.go
Normal file
53
internal/updater/providers/fastestvpn/hosttoserver.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package fastestvpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.FastestvpnServer
|
||||
|
||||
func (hts hostToServer) add(host, country string, tcp, udp bool) {
|
||||
server, ok := hts[host]
|
||||
if !ok {
|
||||
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.FastestvpnServer) {
|
||||
servers = make([]models.FastestvpnServer, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
30
internal/updater/providers/fastestvpn/resolve.go
Normal file
30
internal/updater/providers/fastestvpn/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/updater/providers/fastestvpn/servers.go
Normal file
82
internal/updater/providers/fastestvpn/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.FastestvpnServer, 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/updater/providers/fastestvpn/sort.go
Normal file
16
internal/updater/providers/fastestvpn/sort.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package fastestvpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.FastestvpnServer) {
|
||||
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
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/fastestvpn/string.go
Normal file
14
internal/updater/providers/fastestvpn/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package fastestvpn
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.FastestvpnServer) (s string) {
|
||||
s = "func FastestvpnServers() []models.FastestvpnServer {\n"
|
||||
s += " return []models.FastestvpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
19
internal/updater/providers/hidemyass/hosts.go
Normal file
19
internal/updater/providers/hidemyass/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/updater/providers/hidemyass/hosttourl.go
Normal file
37
internal/updater/providers/hidemyass/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/updater/providers/hidemyass/index.go
Normal file
54
internal/updater/providers/hidemyass/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/updater/providers/hidemyass/resolve.go
Normal file
33
internal/updater/providers/hidemyass/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)
|
||||
}
|
||||
68
internal/updater/providers/hidemyass/servers.go
Normal file
68
internal/updater/providers/hidemyass/servers.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// 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/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.HideMyAssServer, 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.HideMyAssServer, 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.HideMyAssServer{
|
||||
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/updater/providers/hidemyass/sort.go
Normal file
22
internal/updater/providers/hidemyass/sort.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package hidemyass
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.HideMyAssServer) {
|
||||
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
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/hidemyass/string.go
Normal file
14
internal/updater/providers/hidemyass/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package hidemyass
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.HideMyAssServer) (s string) {
|
||||
s = "func HideMyAssServers() []models.HideMyAssServer {\n"
|
||||
s += " return []models.HideMyAssServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
44
internal/updater/providers/hidemyass/url.go
Normal file
44
internal/updater/providers/hidemyass/url.go
Normal file
@@ -0,0 +1,44 @@
|
||||
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]
|
||||
}
|
||||
|
||||
return camelCaseToWords(country), camelCaseToWords(region),
|
||||
camelCaseToWords(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
|
||||
}
|
||||
55
internal/updater/providers/mullvad/api.go
Normal file
55
internal/updater/providers/mullvad/api.go
Normal file
@@ -0,0 +1,55 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
func fetchAPI(ctx context.Context, client *http.Client) (data []serverData, err error) {
|
||||
const url = "https://api.mullvad.net/www/relays/openvpn/"
|
||||
|
||||
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("%w: %s", ErrUnmarshalResponseBody, err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
58
internal/updater/providers/mullvad/hosttoserver.go
Normal file
58
internal/updater/providers/mullvad/hosttoserver.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.MullvadServer
|
||||
|
||||
var (
|
||||
ErrParseIPv4 = errors.New("cannot parse IPv4 address")
|
||||
ErrParseIPv6 = errors.New("cannot parse IPv6 address")
|
||||
)
|
||||
|
||||
func (hts hostToServer) add(data serverData) (err error) {
|
||||
if !data.Active {
|
||||
return
|
||||
}
|
||||
|
||||
ipv4 := net.ParseIP(data.IPv4)
|
||||
if ipv4 == nil || ipv4.To4() == nil {
|
||||
return fmt.Errorf("%w: %s", ErrParseIPv4, data.IPv4)
|
||||
}
|
||||
|
||||
ipv6 := net.ParseIP(data.IPv6)
|
||||
if ipv6 == nil || ipv6.To4() != nil {
|
||||
return fmt.Errorf("%w: %s", ErrParseIPv6, data.IPv6)
|
||||
}
|
||||
|
||||
server, ok := hts[data.Hostname]
|
||||
if !ok {
|
||||
server.Country = data.Country
|
||||
server.City = strings.ReplaceAll(data.City, ",", "")
|
||||
server.ISP = data.Provider
|
||||
server.Owned = data.Owned
|
||||
}
|
||||
|
||||
server.IPs = append(server.IPs, ipv4)
|
||||
server.IPsV6 = append(server.IPsV6, ipv6)
|
||||
|
||||
hts[data.Hostname] = server
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hts hostToServer) toServersSlice() (servers []models.MullvadServer) {
|
||||
servers = make([]models.MullvadServer, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
server.IPs = uniqueSortedIPs(server.IPs)
|
||||
server.IPsV6 = uniqueSortedIPs(server.IPsV6)
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package updater
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -12,6 +12,7 @@ func uniqueSortedIPs(ips []net.IP) []net.IP {
|
||||
key := ip.String()
|
||||
uniqueIPs[key] = struct{}{}
|
||||
}
|
||||
|
||||
ips = make([]net.IP, 0, len(uniqueIPs))
|
||||
for key := range uniqueIPs {
|
||||
ip := net.ParseIP(key)
|
||||
@@ -20,8 +21,10 @@ func uniqueSortedIPs(ips []net.IP) []net.IP {
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
|
||||
sort.Slice(ips, func(i, j int) bool {
|
||||
return bytes.Compare(ips[i], ips[j]) < 0
|
||||
})
|
||||
|
||||
return ips
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package updater
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"net"
|
||||
68
internal/updater/providers/mullvad/servers.go
Normal file
68
internal/updater/providers/mullvad/servers.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Package mullvad contains code to obtain the server information
|
||||
// for the Mullvad provider.
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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.MullvadServer, 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()
|
||||
|
||||
servers = groupByProperties(servers)
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// TODO group by hostname so remove this.
|
||||
func groupByProperties(serversByHost []models.MullvadServer) (serversByProps []models.MullvadServer) {
|
||||
propsToServer := make(map[string]models.MullvadServer, len(serversByHost))
|
||||
for _, server := range serversByHost {
|
||||
key := server.Country + server.City + server.ISP + strconv.FormatBool(server.Owned)
|
||||
serverByProps, ok := propsToServer[key]
|
||||
if !ok {
|
||||
serverByProps.Country = server.Country
|
||||
serverByProps.City = server.City
|
||||
serverByProps.ISP = server.ISP
|
||||
serverByProps.Owned = server.Owned
|
||||
}
|
||||
serverByProps.IPs = append(serverByProps.IPs, server.IPs...)
|
||||
serverByProps.IPsV6 = append(serverByProps.IPsV6, server.IPsV6...)
|
||||
propsToServer[key] = serverByProps
|
||||
}
|
||||
|
||||
serversByProps = make([]models.MullvadServer, 0, len(propsToServer))
|
||||
for _, serverByProp := range propsToServer {
|
||||
serversByProps = append(serversByProps, serverByProp)
|
||||
}
|
||||
|
||||
return serversByProps
|
||||
}
|
||||
19
internal/updater/providers/mullvad/sort.go
Normal file
19
internal/updater/providers/mullvad/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.MullvadServer) {
|
||||
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].ISP < servers[j].ISP
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/mullvad/string.go
Normal file
14
internal/updater/providers/mullvad/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package mullvad
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.MullvadServer) (s string) {
|
||||
s = "func MullvadServers() []models.MullvadServer {\n"
|
||||
s += " return []models.MullvadServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package updater
|
||||
package mullvad
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_stringifyMullvadServers(t *testing.T) {
|
||||
func Test_Stringify(t *testing.T) {
|
||||
servers := []models.MullvadServer{{
|
||||
Country: "webland",
|
||||
City: "webcity",
|
||||
@@ -27,6 +27,6 @@ func MullvadServers() []models.MullvadServer {
|
||||
}
|
||||
`
|
||||
expected = strings.TrimPrefix(strings.TrimSuffix(expected, "\n"), "\n")
|
||||
s := stringifyMullvadServers(servers)
|
||||
s := Stringify(servers)
|
||||
assert.Equal(t, expected, s)
|
||||
}
|
||||
55
internal/updater/providers/nordvpn/api.go
Normal file
55
internal/updater/providers/nordvpn/api.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package nordvpn
|
||||
|
||||
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 {
|
||||
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("%w: %s", ErrUnmarshalResponseBody, err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
16
internal/updater/providers/nordvpn/ip.go
Normal file
16
internal/updater/providers/nordvpn/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/updater/providers/nordvpn/name.go
Normal file
29
internal/updater/providers/nordvpn/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)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: %s", ErrInvalidIDInServerName, serverName)
|
||||
}
|
||||
|
||||
number = uint16(idUint64)
|
||||
return number, nil
|
||||
}
|
||||
64
internal/updater/providers/nordvpn/servers.go
Normal file
64
internal/updater/providers/nordvpn/servers.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Package nordvpn contains code to obtain the server information
|
||||
// for the NordVPN provider.
|
||||
package nordvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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.NordvpnServer, warnings []string, err error) {
|
||||
data, err := fetchAPI(ctx, client)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
servers = make([]models.NordvpnServer, 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.NordvpnServer{
|
||||
Region: jsonServer.Country,
|
||||
Number: number,
|
||||
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/updater/providers/nordvpn/sort.go
Normal file
16
internal/updater/providers/nordvpn/sort.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package nordvpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.NordvpnServer) {
|
||||
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
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/nordvpn/string.go
Normal file
14
internal/updater/providers/nordvpn/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package nordvpn
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.NordvpnServer) (s string) {
|
||||
s = "func NordvpnServers() []models.NordvpnServer {\n"
|
||||
s += " return []models.NordvpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
74
internal/updater/providers/pia/api.go
Normal file
74
internal/updater/providers/pia/api.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package pia
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"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"`
|
||||
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: %s", ErrHTTPStatusCodeNotOK, response.Status)
|
||||
}
|
||||
|
||||
b, err := ioutil.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
|
||||
}
|
||||
90
internal/updater/providers/pia/servers.go
Normal file
90
internal/updater/providers/pia/servers.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Package pia contains code to obtain the server information
|
||||
// for the Private Internet Access provider.
|
||||
package pia
|
||||
|
||||
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.PIAServer, err error) {
|
||||
data, err := fetchAPI(ctx, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, region := range data.Regions {
|
||||
// Deduplicate servers with the same common name
|
||||
commonNameToProtocols := dedupByProtocol(region)
|
||||
|
||||
// newServers can support only UDP or both TCP and UDP
|
||||
newServers := dataToServers(region.Servers.UDP, region.Name,
|
||||
region.PortForward, commonNameToProtocols)
|
||||
servers = append(servers, newServers...)
|
||||
|
||||
// tcpServers only support TCP as mixed servers were found above.
|
||||
tcpServers := dataToServers(region.Servers.TCP, region.Name,
|
||||
region.PortForward, commonNameToProtocols)
|
||||
servers = append(servers, tcpServers...)
|
||||
}
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
type protocols struct {
|
||||
tcp bool
|
||||
udp bool
|
||||
}
|
||||
|
||||
// Deduplicate servers with the same common name for different protocols.
|
||||
func dedupByProtocol(region regionData) (commonNameToProtocols map[string]protocols) {
|
||||
commonNameToProtocols = make(map[string]protocols)
|
||||
for _, udpServer := range region.Servers.UDP {
|
||||
protocols := commonNameToProtocols[udpServer.CN]
|
||||
protocols.udp = true
|
||||
commonNameToProtocols[udpServer.CN] = protocols
|
||||
}
|
||||
for _, tcpServer := range region.Servers.TCP {
|
||||
protocols := commonNameToProtocols[tcpServer.CN]
|
||||
protocols.tcp = true
|
||||
commonNameToProtocols[tcpServer.CN] = protocols
|
||||
}
|
||||
return commonNameToProtocols
|
||||
}
|
||||
|
||||
func dataToServers(data []serverData, region string, portForward bool,
|
||||
commonNameToProtocols map[string]protocols) (
|
||||
servers []models.PIAServer) {
|
||||
servers = make([]models.PIAServer, 0, len(data))
|
||||
for _, serverData := range data {
|
||||
proto, ok := commonNameToProtocols[serverData.CN]
|
||||
if !ok {
|
||||
continue // server already added
|
||||
}
|
||||
delete(commonNameToProtocols, serverData.CN)
|
||||
server := models.PIAServer{
|
||||
Region: region,
|
||||
ServerName: serverData.CN,
|
||||
TCP: proto.tcp,
|
||||
UDP: proto.udp,
|
||||
PortForward: portForward,
|
||||
IP: serverData.IP,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
16
internal/updater/providers/pia/sort.go
Normal file
16
internal/updater/providers/pia/sort.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package pia
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.PIAServer) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Region == servers[j].Region {
|
||||
return servers[i].ServerName < servers[j].ServerName
|
||||
}
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/pia/string.go
Normal file
14
internal/updater/providers/pia/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package pia
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.PIAServer) (s string) {
|
||||
s = "func PIAServers() []models.PIAServer {\n"
|
||||
s += " return []models.PIAServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
53
internal/updater/providers/privado/hosttoserver.go
Normal file
53
internal/updater/providers/privado/hosttoserver.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package privado
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.PrivadoServer
|
||||
|
||||
func (hts hostToServer) add(host string) {
|
||||
server, ok := hts[host]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
server.Hostname = host
|
||||
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) (
|
||||
warnings []string) {
|
||||
for host, IPs := range hostToIPs {
|
||||
if len(IPs) > 1 {
|
||||
warning := "more than one IP address found for host " + host
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
server := hts[host]
|
||||
server.IP = IPs[0]
|
||||
hts[host] = server
|
||||
}
|
||||
for host, server := range hts {
|
||||
if server.IP == nil {
|
||||
delete(hts, host)
|
||||
}
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
func (hts hostToServer) toServersSlice() (servers []models.PrivadoServer) {
|
||||
servers = make([]models.PrivadoServer, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
31
internal/updater/providers/privado/resolve.go
Normal file
31
internal/updater/providers/privado/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 = 3 * time.Second
|
||||
maxNoNew = 1
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
72
internal/updater/providers/privado/servers.go
Normal file
72
internal/updater/providers/privado/servers.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Package privado contains code to obtain the server information
|
||||
// for the Privado provider.
|
||||
package privado
|
||||
|
||||
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.PrivadoServer, 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
|
||||
}
|
||||
|
||||
newWarnings = hts.adaptWithIPs(hostToIPs)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
13
internal/updater/providers/privado/sort.go
Normal file
13
internal/updater/providers/privado/sort.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package privado
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.PrivadoServer) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/privado/string.go
Normal file
14
internal/updater/providers/privado/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package privado
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.PrivadoServer) (s string) {
|
||||
s = "func PrivadoServers() []models.PrivadoServer {\n"
|
||||
s += " return []models.PrivadoServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
14
internal/updater/providers/privatevpn/countries.go
Normal file
14
internal/updater/providers/privatevpn/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
|
||||
}
|
||||
54
internal/updater/providers/privatevpn/filename.go
Normal file
54
internal/updater/providers/privatevpn/filename.go
Normal file
@@ -0,0 +1,54 @@
|
||||
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)
|
||||
|
||||
return countryCode, city, nil
|
||||
}
|
||||
50
internal/updater/providers/privatevpn/hosttoserver.go
Normal file
50
internal/updater/providers/privatevpn/hosttoserver.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package privatevpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.PrivatevpnServer
|
||||
|
||||
// TODO check if server supports TCP and UDP.
|
||||
func (hts hostToServer) add(host, country, city string) {
|
||||
server, ok := hts[host]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
server.Hostname = host
|
||||
server.Country = country
|
||||
server.City = city
|
||||
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.PrivatevpnServer) {
|
||||
servers = make([]models.PrivatevpnServer, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
33
internal/updater/providers/privatevpn/resolve.go
Normal file
33
internal/updater/providers/privatevpn/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)
|
||||
}
|
||||
86
internal/updater/providers/privatevpn/servers.go
Normal file
86
internal/updater/providers/privatevpn/servers.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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.PrivatevpnServer, 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)
|
||||
|
||||
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 {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error() + " in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
hts.add(host, country, city)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
19
internal/updater/providers/privatevpn/sort.go
Normal file
19
internal/updater/providers/privatevpn/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package privatevpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.PrivatevpnServer) {
|
||||
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
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/privatevpn/string.go
Normal file
14
internal/updater/providers/privatevpn/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package privatevpn
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.PrivatevpnServer) (s string) {
|
||||
s = "func PrivatevpnServers() []models.PrivatevpnServer {\n"
|
||||
s += " return []models.PrivatevpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
65
internal/updater/providers/protonvpn/api.go
Normal file
65
internal/updater/providers/protonvpn/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")
|
||||
ErrUnmarshalResponseBody = errors.New("failed unmarshaling response body")
|
||||
)
|
||||
|
||||
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: %s", ErrHTTPStatusCodeNotOK, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return data, fmt.Errorf("%w: %s", ErrUnmarshalResponseBody, err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
14
internal/updater/providers/protonvpn/countries.go
Normal file
14
internal/updater/providers/protonvpn/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
|
||||
}
|
||||
98
internal/updater/providers/protonvpn/servers.go
Normal file
98
internal/updater/providers/protonvpn/servers.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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.ProtonvpnServer, 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)
|
||||
}
|
||||
|
||||
servers = make([]models.ProtonvpnServer, 0, count)
|
||||
for _, logicalServer := range data.LogicalServers {
|
||||
for _, physicalServer := range logicalServer.Servers {
|
||||
server, warning, err := makeServer(
|
||||
physicalServer, logicalServer, countryCodes)
|
||||
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
warnings = append(warnings, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var errServerStatusZero = errors.New("ignoring server with status 0")
|
||||
|
||||
func makeServer(physical physicalServer, logical logicalServer,
|
||||
countryCodes map[string]string) (server models.ProtonvpnServer,
|
||||
warning string, err error) {
|
||||
if physical.Status == 0 {
|
||||
return server, "", fmt.Errorf("%w: %s",
|
||||
errServerStatusZero, physical.Domain)
|
||||
}
|
||||
|
||||
countryCode := logical.ExitCountry
|
||||
country, warning := codeToCountry(countryCode, countryCodes)
|
||||
|
||||
server = models.ProtonvpnServer{
|
||||
// Note: for multi-hop use the server name or hostname
|
||||
// instead of the country
|
||||
Country: country,
|
||||
Region: getStringValue(logical.Region),
|
||||
City: getStringValue(logical.City),
|
||||
Name: logical.Name,
|
||||
Hostname: physical.Domain,
|
||||
EntryIP: physical.EntryIP,
|
||||
ExitIP: physical.ExitIP,
|
||||
}
|
||||
|
||||
return server, warning, nil
|
||||
}
|
||||
|
||||
func getStringValue(ptr *string) string {
|
||||
if ptr == nil {
|
||||
return ""
|
||||
}
|
||||
return *ptr
|
||||
}
|
||||
26
internal/updater/providers/protonvpn/sort.go
Normal file
26
internal/updater/providers/protonvpn/sort.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package protonvpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.ProtonvpnServer) {
|
||||
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.Name == b.Name {
|
||||
return a.Hostname < b.Hostname
|
||||
}
|
||||
return a.Name < b.Name
|
||||
}
|
||||
return a.City < b.City
|
||||
}
|
||||
return a.Region < b.Region
|
||||
}
|
||||
return a.Country < b.Country
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/protonvpn/string.go
Normal file
14
internal/updater/providers/protonvpn/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package protonvpn
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.ProtonvpnServer) (s string) {
|
||||
s = "func ProtonvpnServers() []models.ProtonvpnServer {\n"
|
||||
s += " return []models.ProtonvpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
43
internal/updater/providers/purevpn/hosttoserver.go
Normal file
43
internal/updater/providers/purevpn/hosttoserver.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package purevpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.PurevpnServer
|
||||
|
||||
func (hts hostToServer) add(host string) {
|
||||
// TODO set TCP and UDP compatibility, set hostname
|
||||
hts[host] = models.PurevpnServer{}
|
||||
}
|
||||
|
||||
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.PurevpnServer) {
|
||||
servers = make([]models.PurevpnServer, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
33
internal/updater/providers/purevpn/locationtoserver.go
Normal file
33
internal/updater/providers/purevpn/locationtoserver.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package purevpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type locationToServer map[string]models.PurevpnServer
|
||||
|
||||
func locationKey(country, region, city string) string {
|
||||
return country + region + city
|
||||
}
|
||||
|
||||
func (lts locationToServer) add(country, region, city string, ips []net.IP) {
|
||||
key := locationKey(country, region, city)
|
||||
server, ok := lts[key]
|
||||
if !ok {
|
||||
server.Country = country
|
||||
server.Region = region
|
||||
server.City = city
|
||||
}
|
||||
server.IPs = append(server.IPs, ips...)
|
||||
lts[key] = server
|
||||
}
|
||||
|
||||
func (lts locationToServer) toServersSlice() (servers []models.PurevpnServer) {
|
||||
servers = make([]models.PurevpnServer, 0, len(lts))
|
||||
for _, server := range lts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
33
internal/updater/providers/purevpn/resolve.go
Normal file
33
internal/updater/providers/purevpn/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)
|
||||
}
|
||||
98
internal/updater/providers/purevpn/servers.go
Normal file
98
internal/updater/providers/purevpn/servers.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Package purevpn contains code to obtain the server information
|
||||
// for the PureVPN provider.
|
||||
package purevpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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.PurevpnServer, warnings []string, err error) {
|
||||
const url = "https://s3-us-west-1.amazonaws.com/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
|
||||
}
|
||||
|
||||
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 len(servers) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
// Dedup by location
|
||||
lts := make(locationToServer)
|
||||
for _, server := range servers {
|
||||
country, region, city, err := publicip.Info(ctx, client, server.IPs[0])
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
// TODO split servers by host
|
||||
lts.add(country, region, city, server.IPs)
|
||||
}
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
servers = lts.toServersSlice()
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
19
internal/updater/providers/purevpn/sort.go
Normal file
19
internal/updater/providers/purevpn/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package purevpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.PurevpnServer) {
|
||||
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
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/purevpn/string.go
Normal file
14
internal/updater/providers/purevpn/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package purevpn
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.PurevpnServer) (s string) {
|
||||
s = "func PurevpnServers() []models.PurevpnServer {\n"
|
||||
s += " return []models.PurevpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
53
internal/updater/providers/surfshark/api.go
Normal file
53
internal/updater/providers/surfshark/api.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
ErrUnmarshalResponseBody = errors.New("failed unmarshaling response body")
|
||||
)
|
||||
|
||||
//nolint:unused
|
||||
type serverData struct {
|
||||
Host string `json:"connectionName"`
|
||||
Country string `json:"country"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
//nolint:unused,deadcode
|
||||
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: %s", ErrHTTPStatusCodeNotOK, response.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&servers); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponseBody, err)
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
59
internal/updater/providers/surfshark/hosttoserver.go
Normal file
59
internal/updater/providers/surfshark/hosttoserver.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.SurfsharkServer
|
||||
|
||||
func (hts hostToServer) add(host, region string) {
|
||||
// TODO set TCP and UDP
|
||||
// TODO set hostname
|
||||
server, ok := hts[host]
|
||||
if !ok {
|
||||
server.Region = region
|
||||
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) toSubdomainsSlice() (subdomains []string) {
|
||||
subdomains = make([]string, 0, len(hts))
|
||||
const suffix = ".prod.surfshark.com"
|
||||
for host := range hts {
|
||||
subdomain := strings.TrimSuffix(host, suffix)
|
||||
subdomains = append(subdomains, subdomain)
|
||||
}
|
||||
return subdomains
|
||||
}
|
||||
|
||||
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.SurfsharkServer) {
|
||||
servers = make([]models.SurfsharkServer, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
200
internal/updater/providers/surfshark/regions.go
Normal file
200
internal/updater/providers/surfshark/regions.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errSuffixNotFound = errors.New("suffix not found")
|
||||
errSubdomainNotFound = errors.New("subdomain not found in subdomain to region mapping")
|
||||
)
|
||||
|
||||
func parseHost(host string, subdomainToRegion map[string]string) (
|
||||
region string, err error) {
|
||||
const suffix = ".prod.surfshark.com"
|
||||
if !strings.HasSuffix(host, suffix) {
|
||||
return "", fmt.Errorf("%w: %s in %s",
|
||||
errSuffixNotFound, suffix, host)
|
||||
}
|
||||
|
||||
subdomain := strings.TrimSuffix(host, suffix)
|
||||
region, ok := subdomainToRegion[subdomain]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %s", errSubdomainNotFound, subdomain)
|
||||
}
|
||||
|
||||
return region, nil
|
||||
}
|
||||
|
||||
func subdomainToRegion() (mapping map[string]string) {
|
||||
return map[string]string{
|
||||
"ae-dub": "United Arab Emirates",
|
||||
"al-tia": "Albania",
|
||||
"at-vie": "Austria",
|
||||
"au-adl": "Australia Adelaide",
|
||||
"au-bne": "Australia Brisbane",
|
||||
"au-mel": "Australia Melbourne",
|
||||
"au-per": "Australia Perth",
|
||||
"au-syd": "Australia Sydney",
|
||||
"au-us": "Australia US",
|
||||
"az-bak": "Azerbaijan",
|
||||
"ba-sjj": "Bosnia and Herzegovina",
|
||||
"be-bru": "Belgium",
|
||||
"bg-sof": "Bulgaria",
|
||||
"br-sao": "Brazil",
|
||||
"ca-mon": "Canada Montreal",
|
||||
"ca-tor": "Canada Toronto",
|
||||
"ca-us": "Canada US",
|
||||
"ca-van": "Canada Vancouver",
|
||||
"ch-zur": "Switzerland",
|
||||
"cl-san": "Chile",
|
||||
"co-bog": "Colombia",
|
||||
"cr-sjn": "Costa Rica",
|
||||
"cy-nic": "Cyprus",
|
||||
"cz-prg": "Czech Republic",
|
||||
"de-ber": "Germany Berlin",
|
||||
"de-fra": "Germany Frankfurt am Main",
|
||||
"de-fra-st001": "Germany Frankfurt am Main st001",
|
||||
"de-fra-st002": "Germany Frankfurt am Main st002",
|
||||
"de-fra-st003": "Germany Frankfurt am Main st003",
|
||||
"de-fra-st004": "Germany Frankfurt am Main st004",
|
||||
"de-fra-st005": "Germany Frankfurt am Main st005",
|
||||
"de-muc": "Germany Munich",
|
||||
"de-nue": "Germany Nuremberg",
|
||||
"de-sg": "Germany Singapour",
|
||||
"de-uk": "Germany UK",
|
||||
"dk-cph": "Denmark",
|
||||
"ee-tll": "Estonia",
|
||||
"es-bcn": "Spain Barcelona",
|
||||
"es-mad": "Spain Madrid",
|
||||
"es-vlc": "Spain Valencia",
|
||||
"fi-hel": "Finland",
|
||||
"fr-bod": "France Bordeaux",
|
||||
"fr-mrs": "France Marseilles",
|
||||
"fr-par": "France Paris",
|
||||
"fr-se": "France Sweden",
|
||||
"gr-ath": "Greece",
|
||||
"hk-hkg": "Hong Kong",
|
||||
"hr-zag": "Croatia",
|
||||
"hu-bud": "Hungary",
|
||||
"id-jak": "Indonesia",
|
||||
"ie-dub": "Ireland",
|
||||
"il-tlv": "Israel",
|
||||
"in-chn": "India Chennai",
|
||||
"in-idr": "India Indore",
|
||||
"in-mum": "India Mumbai",
|
||||
"in-uk": "India UK",
|
||||
"is-rkv": "Iceland",
|
||||
"it-mil": "Italy Milan",
|
||||
"it-rom": "Italy Rome",
|
||||
"jp-tok": "Japan Tokyo",
|
||||
"jp-tok-st001": "Japan Tokyo st001",
|
||||
"jp-tok-st002": "Japan Tokyo st002",
|
||||
"jp-tok-st003": "Japan Tokyo st003",
|
||||
"jp-tok-st004": "Japan Tokyo st004",
|
||||
"jp-tok-st005": "Japan Tokyo st005",
|
||||
"jp-tok-st006": "Japan Tokyo st006",
|
||||
"jp-tok-st007": "Japan Tokyo st007",
|
||||
"jp-tok-st008": "Japan Tokyo st008",
|
||||
"jp-tok-st009": "Japan Tokyo st009",
|
||||
"jp-tok-st010": "Japan Tokyo st010",
|
||||
"jp-tok-st011": "Japan Tokyo st011",
|
||||
"jp-tok-st012": "Japan Tokyo st012",
|
||||
"jp-tok-st013": "Japan Tokyo st013",
|
||||
"kr-seo": "Korea",
|
||||
"kz-ura": "Kazakhstan",
|
||||
"lu-ste": "Luxembourg",
|
||||
"lv-rig": "Latvia",
|
||||
"ly-tip": "Libya",
|
||||
"md-chi": "Moldova",
|
||||
"mk-skp": "North Macedonia",
|
||||
"my-kul": "Malaysia",
|
||||
"ng-lag": "Nigeria",
|
||||
"nl-ams": "Netherlands Amsterdam",
|
||||
"nl-ams-st001": "Netherlands Amsterdam st001",
|
||||
"nl-us": "Netherlands US",
|
||||
"no-osl": "Norway",
|
||||
"nz-akl": "New Zealand",
|
||||
"ph-mnl": "Philippines",
|
||||
"pl-gdn": "Poland Gdansk",
|
||||
"pl-waw": "Poland Warsaw",
|
||||
"pt-lis": "Portugal Lisbon",
|
||||
"pt-lou": "Portugal Loule",
|
||||
"pt-opo": "Portugal Porto",
|
||||
"py-asu": "Paraguay",
|
||||
"ro-buc": "Romania",
|
||||
"rs-beg": "Serbia",
|
||||
"ru-mos": "Russia Moscow",
|
||||
"ru-spt": "Russia St. Petersburg",
|
||||
"se-sto": "Sweden",
|
||||
"sg-hk": "Singapore Hong Kong",
|
||||
"sg-nl": "Singapore Netherlands",
|
||||
"sg-sng": "Singapore",
|
||||
"sg-in": "Singapore in",
|
||||
"sg-sng-st001": "Singapore st001",
|
||||
"sg-sng-st002": "Singapore st002",
|
||||
"sg-sng-st003": "Singapore st003",
|
||||
"sg-sng-st004": "Singapore st004",
|
||||
"sg-sng-mp001": "Singapore mp001",
|
||||
"si-lju": "Slovenia",
|
||||
"sk-bts": "Slovekia",
|
||||
"th-bkk": "Thailand",
|
||||
"tr-bur": "Turkey",
|
||||
"tw-tai": "Taiwan",
|
||||
"ua-iev": "Ukraine",
|
||||
"uk-de": "UK Germany",
|
||||
"uk-fr": "UK France",
|
||||
"uk-gla": "UK Glasgow",
|
||||
"uk-lon": "UK London",
|
||||
"uk-lon-mp001": "UK London mp001",
|
||||
"uk-lon-st001": "UK London st001",
|
||||
"uk-lon-st002": "UK London st002",
|
||||
"uk-lon-st003": "UK London st003",
|
||||
"uk-lon-st004": "UK London st004",
|
||||
"uk-lon-st005": "UK London st005",
|
||||
"uk-man": "UK Manchester",
|
||||
"us-atl": "US Atlanta",
|
||||
"us-bdn": "US Bend",
|
||||
"us-bos": "US Boston",
|
||||
"us-buf": "US Buffalo",
|
||||
"us-chi": "US Chicago",
|
||||
"us-clt": "US Charlotte",
|
||||
"us-dal": "US Dallas",
|
||||
"us-den": "US Denver",
|
||||
"us-dtw": "US Gahanna",
|
||||
"us-hou": "US Houston",
|
||||
"us-kan": "US Kansas City",
|
||||
"us-las": "US Las Vegas",
|
||||
"us-lax": "US Los Angeles",
|
||||
"us-ltm": "US Latham",
|
||||
"us-mia": "US Miami",
|
||||
"us-mnz": "US Maryland",
|
||||
"us-nl": "US Netherlands",
|
||||
"us-nyc": "US New York City",
|
||||
"us-nyc-mp001": "US New York City mp001",
|
||||
"us-nyc-st001": "US New York City st001",
|
||||
"us-nyc-st002": "US New York City st002",
|
||||
"us-nyc-st003": "US New York City st003",
|
||||
"us-nyc-st004": "US New York City st004",
|
||||
"us-nyc-st005": "US New York City st005",
|
||||
"us-orl": "US Orlando",
|
||||
"us-phx": "US Phoenix",
|
||||
"us-pt": "US Portugal",
|
||||
"us-sea": "US Seatle",
|
||||
"us-sfo": "US San Francisco",
|
||||
"us-slc": "US Salt Lake City",
|
||||
"us-stl": "US Saint Louis",
|
||||
"us-tpa": "US Tampa",
|
||||
"vn-hcm": "Vietnam",
|
||||
"za-jnb": "South Africa",
|
||||
"ar-bua": "Argentina Buenos Aires",
|
||||
"tr-ist": "Turkey Istanbul",
|
||||
"mx-mex": "Mexico City Mexico",
|
||||
"ca-tor-mp001": "Canada Toronto mp001",
|
||||
"de-fra-mp001": "Germany Frankfurt mp001",
|
||||
"nl-ams-mp001": "Netherlands Amsterdam mp001",
|
||||
"us-sfo-mp001": "US San Francisco mp001",
|
||||
}
|
||||
}
|
||||
32
internal/updater/providers/surfshark/resolve.go
Normal file
32
internal/updater/providers/surfshark/resolve.go
Normal file
@@ -0,0 +1,32 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
130
internal/updater/providers/surfshark/servers.go
Normal file
130
internal/updater/providers/surfshark/servers.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Package surfshark contains code to obtain the server information
|
||||
// for the Surshark provider.
|
||||
package surfshark
|
||||
|
||||
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.SurfsharkServer, 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, nil, err
|
||||
} else if len(contents) < minServers {
|
||||
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(contents), minServers)
|
||||
}
|
||||
|
||||
subdomainToRegion := subdomainToRegion()
|
||||
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
|
||||
}
|
||||
|
||||
region, err := parseHost(host, subdomainToRegion)
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warning := err.Error()
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
hts.add(host, region)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// process subdomain entries in mapping that were not in the Zip file
|
||||
subdomainsDone := hts.toSubdomainsSlice()
|
||||
for _, subdomainDone := range subdomainsDone {
|
||||
delete(subdomainToRegion, subdomainDone)
|
||||
}
|
||||
remainingServers, newWarnings, err := getRemainingServers(
|
||||
ctx, subdomainToRegion, presolver)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
servers = append(servers, remainingServers...)
|
||||
|
||||
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 getRemainingServers(ctx context.Context,
|
||||
subdomainToRegionLeft map[string]string, presolver resolver.Parallel) (
|
||||
servers []models.SurfsharkServer, warnings []string, err error) {
|
||||
hosts := make([]string, 0, len(subdomainToRegionLeft))
|
||||
const suffix = ".prod.surfshark.com"
|
||||
for subdomain := range subdomainToRegionLeft {
|
||||
hosts = append(hosts, subdomain+suffix)
|
||||
}
|
||||
|
||||
const minServers = 0
|
||||
hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
servers = make([]models.SurfsharkServer, 0, len(hostToIPs))
|
||||
for host, IPs := range hostToIPs {
|
||||
region, err := parseHost(host, subdomainToRegionLeft)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
server := models.SurfsharkServer{
|
||||
Region: region,
|
||||
IPs: IPs,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
13
internal/updater/providers/surfshark/sort.go
Normal file
13
internal/updater/providers/surfshark/sort.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package surfshark
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.SurfsharkServer) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/surfshark/string.go
Normal file
14
internal/updater/providers/surfshark/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package surfshark
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.SurfsharkServer) (s string) {
|
||||
s = "func SurfsharkServers() []models.SurfsharkServer {\n"
|
||||
s += " return []models.SurfsharkServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
31
internal/updater/providers/torguard/filename.go
Normal file
31
internal/updater/providers/torguard/filename.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package torguard
|
||||
|
||||
import "strings"
|
||||
|
||||
func parseFilename(fileName string) (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 = strings.Title(s)
|
||||
city = s
|
||||
|
||||
default:
|
||||
country = s
|
||||
}
|
||||
|
||||
return country, city
|
||||
}
|
||||
76
internal/updater/providers/torguard/servers.go
Normal file
76
internal/updater/providers/torguard/servers.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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/unzip"
|
||||
)
|
||||
|
||||
var ErrNotEnoughServers = errors.New("not enough servers found")
|
||||
|
||||
func GetServers(ctx context.Context, unzipper unzip.Unzipper, minServers int) (
|
||||
servers []models.TorguardServer, warnings []string, err error) {
|
||||
const url = "https://torguard.net/downloads/OpenVPN-TCP-Linux.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)
|
||||
}
|
||||
|
||||
servers = make([]models.TorguardServer, 0, len(contents))
|
||||
for fileName, content := range contents {
|
||||
if !strings.HasSuffix(fileName, ".ovpn") {
|
||||
continue // not an OpenVPN file
|
||||
}
|
||||
|
||||
country, city := parseFilename(fileName)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ip, warning, err := openvpn.ExtractIP(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
|
||||
}
|
||||
|
||||
server := models.TorguardServer{
|
||||
Country: country,
|
||||
City: city,
|
||||
Hostname: host,
|
||||
IP: ip,
|
||||
}
|
||||
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
|
||||
}
|
||||
19
internal/updater/providers/torguard/sort.go
Normal file
19
internal/updater/providers/torguard/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package torguard
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.TorguardServer) {
|
||||
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
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/torguard/string.go
Normal file
14
internal/updater/providers/torguard/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package torguard
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.TorguardServer) (s string) {
|
||||
s = "func TorguardServers() []models.TorguardServer {\n"
|
||||
s += " return []models.TorguardServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
22
internal/updater/providers/vyprvpn/filename.go
Normal file
22
internal/updater/providers/vyprvpn/filename.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package vyprvpn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var errNotOvpnExt = errors.New("filename does not have the openvpn file extension")
|
||||
|
||||
func parseFilename(fileName string) (
|
||||
region string, err error,
|
||||
) {
|
||||
const suffix = ".ovpn"
|
||||
if !strings.HasSuffix(fileName, suffix) {
|
||||
return "", fmt.Errorf("%w: %s", errNotOvpnExt, fileName)
|
||||
}
|
||||
|
||||
region = strings.TrimSuffix(fileName, suffix)
|
||||
region = strings.ReplaceAll(region, " - ", " ")
|
||||
return region, nil
|
||||
}
|
||||
47
internal/updater/providers/vyprvpn/hosttoserver.go
Normal file
47
internal/updater/providers/vyprvpn/hosttoserver.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package vyprvpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.VyprvpnServer
|
||||
|
||||
func (hts hostToServer) add(host, region string) {
|
||||
server, ok := hts[host]
|
||||
// TODO set host
|
||||
if !ok {
|
||||
server.Region = region
|
||||
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.VyprvpnServer) {
|
||||
servers = make([]models.VyprvpnServer, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
30
internal/updater/providers/vyprvpn/resolve.go
Normal file
30
internal/updater/providers/vyprvpn/resolve.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package vyprvpn
|
||||
|
||||
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 = 2
|
||||
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/updater/providers/vyprvpn/servers.go
Normal file
82
internal/updater/providers/vyprvpn/servers.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Package vyprvpn contains code to obtain the server information
|
||||
// for the VyprVPN provider.
|
||||
package vyprvpn
|
||||
|
||||
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.VyprvpnServer, warnings []string, err error) {
|
||||
const url = "https://support.vyprvpn.com/hc/article_attachments/360052617332/Vypr_OpenVPN_20200320.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
|
||||
}
|
||||
|
||||
region, err := parseFilename(fileName)
|
||||
if err != nil {
|
||||
warnings = append(warnings, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
hts.add(host, region)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
13
internal/updater/providers/vyprvpn/sort.go
Normal file
13
internal/updater/providers/vyprvpn/sort.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package vyprvpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.VyprvpnServer) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/vyprvpn/string.go
Normal file
14
internal/updater/providers/vyprvpn/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package vyprvpn
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.VyprvpnServer) (s string) {
|
||||
s = "func VyprvpnServers() []models.VyprvpnServer {\n"
|
||||
s += " return []models.VyprvpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
61
internal/updater/providers/windscribe/api.go
Normal file
61
internal/updater/providers/windscribe/api.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package windscribe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
|
||||
ErrUnmarshalResponseBody = errors.New("failed unmarshaling response body")
|
||||
)
|
||||
|
||||
type apiData struct {
|
||||
Data []regionData `json:"data"`
|
||||
}
|
||||
|
||||
type regionData struct {
|
||||
Region string `json:"name"`
|
||||
Groups []groupData `json:"groups"`
|
||||
}
|
||||
|
||||
type groupData struct {
|
||||
City string `json:"city"`
|
||||
Nodes []serverData `json:"nodes"`
|
||||
}
|
||||
|
||||
type serverData struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OpenvpnIP net.IP `json:"ip2"`
|
||||
}
|
||||
|
||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
||||
data apiData, err error) {
|
||||
const baseURL = "https://assets.windscribe.com/serverlist/mob-v2/1/"
|
||||
cacheBreaker := time.Now().Unix()
|
||||
url := baseURL + strconv.Itoa(int(cacheBreaker))
|
||||
|
||||
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()
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return data, fmt.Errorf("%w: %s", ErrUnmarshalResponseBody, err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
47
internal/updater/providers/windscribe/servers.go
Normal file
47
internal/updater/providers/windscribe/servers.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Package windscribe contains code to obtain the server information
|
||||
// for the Windscribe provider.
|
||||
package windscribe
|
||||
|
||||
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.WindscribeServer, err error) {
|
||||
data, err := fetchAPI(ctx, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, regionData := range data.Data {
|
||||
region := regionData.Region
|
||||
for _, group := range regionData.Groups {
|
||||
city := group.City
|
||||
for _, node := range group.Nodes {
|
||||
server := models.WindscribeServer{
|
||||
Region: region,
|
||||
City: city,
|
||||
Hostname: node.Hostname,
|
||||
IP: node.OpenvpnIP,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
19
internal/updater/providers/windscribe/sort.go
Normal file
19
internal/updater/providers/windscribe/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package windscribe
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.WindscribeServer) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
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
|
||||
})
|
||||
}
|
||||
14
internal/updater/providers/windscribe/string.go
Normal file
14
internal/updater/providers/windscribe/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package windscribe
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.WindscribeServer) (s string) {
|
||||
s = "func WindscribeServers() []models.WindscribeServer {\n"
|
||||
s += " return []models.WindscribeServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/publicip"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func (u *updater) updatePurevpn(ctx context.Context) (err error) {
|
||||
servers, warnings, err := findPurevpnServers(ctx, u.client, u.presolver)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("PureVPN: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update Purevpn servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyPurevpnServers(servers))
|
||||
}
|
||||
u.servers.Purevpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Purevpn.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func findPurevpnServers(ctx context.Context, client *http.Client, presolver resolver.Parallel) (
|
||||
servers []models.PurevpnServer, warnings []string, err error) {
|
||||
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
|
||||
}
|
||||
|
||||
hosts := make([]string, 0, len(contents))
|
||||
for fileName, content := range contents {
|
||||
if strings.HasSuffix(fileName, "-tcp.ovpn") {
|
||||
continue // only parse UDP files
|
||||
}
|
||||
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)
|
||||
}
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
hostToIPs, newWarnings, err := presolver.Resolve(ctx, hosts, settings)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
uniqueServers := make(map[string]models.PurevpnServer, len(hostToIPs))
|
||||
for host, IPs := range hostToIPs {
|
||||
if len(IPs) == 0 {
|
||||
warning := fmt.Sprintf("no IP address found for host %q", host)
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
|
||||
country, region, city, err := publicip.Info(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
|
||||
}
|
||||
|
||||
func stringifyPurevpnServers(servers []models.PurevpnServer) (s string) {
|
||||
s = "func PurevpnServers() []models.PurevpnServer {\n"
|
||||
s += " return []models.PurevpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
@@ -1,417 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func (u *updater) updateSurfshark(ctx context.Context) (err error) {
|
||||
servers, warnings, err := findSurfsharkServersFromZip(ctx, u.client, u.presolver)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Surfshark: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update Surfshark servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifySurfsharkServers(servers))
|
||||
}
|
||||
u.servers.Surfshark.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Surfshark.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:deadcode,unused
|
||||
func findSurfsharkServersFromAPI(ctx context.Context, client *http.Client, presolver resolver.Parallel) (
|
||||
servers []models.SurfsharkServer, warnings []string, 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, nil, err
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("%w: %s for %s", ErrHTTPStatusCodeNotOK, response.Status, url)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var jsonServers []struct {
|
||||
Host string `json:"connectionName"`
|
||||
Country string `json:"country"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
if err := decoder.Decode(&jsonServers); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
hosts := make([]string, len(jsonServers))
|
||||
for i := range jsonServers {
|
||||
hosts[i] = jsonServers[i].Host
|
||||
}
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
},
|
||||
}
|
||||
hostToIPs, warnings, err := presolver.Resolve(ctx, hosts, settings)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
for _, jsonServer := range jsonServers {
|
||||
host := jsonServer.Host
|
||||
IPs := hostToIPs[host]
|
||||
if len(IPs) == 0 {
|
||||
warning := fmt.Sprintf("no IP address found for host %q", host)
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
server := models.SurfsharkServer{
|
||||
Region: jsonServer.Country + " " + jsonServer.Location,
|
||||
IPs: IPs,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func findSurfsharkServersFromZip(ctx context.Context, client *http.Client, presolver resolver.Parallel) (
|
||||
servers []models.SurfsharkServer, warnings []string, err error) {
|
||||
const zipURL = "https://my.surfshark.com/vpn/api/v1/server/configurations"
|
||||
contents, err := fetchAndExtractFiles(ctx, client, zipURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mapping := surfsharkSubdomainToRegion()
|
||||
hosts := make([]string, 0, len(contents))
|
||||
for fileName, content := range contents {
|
||||
if strings.HasSuffix(fileName, "_tcp.ovpn") {
|
||||
continue // only parse UDP files
|
||||
}
|
||||
host, warning, err := extractHostFromOVPN(content)
|
||||
if len(warning) > 0 {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
warnings = append(warnings, err.Error()+" in "+fileName)
|
||||
continue
|
||||
}
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
},
|
||||
}
|
||||
hostToIPs, newWarnings, err := presolver.Resolve(ctx, hosts, settings)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
for host, IPs := range hostToIPs {
|
||||
if len(IPs) == 0 {
|
||||
warning := fmt.Sprintf("no IP address found for host %q", host)
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
subdomain := strings.TrimSuffix(host, ".prod.surfshark.com")
|
||||
region, ok := mapping[subdomain]
|
||||
if ok {
|
||||
delete(mapping, subdomain)
|
||||
} else {
|
||||
region = strings.TrimSuffix(host, ".prod.surfshark.com")
|
||||
warning := fmt.Sprintf("subdomain %q not found in Surfshark mapping", subdomain)
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
server := models.SurfsharkServer{
|
||||
Region: region,
|
||||
IPs: IPs,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
// process entries in mapping that were not in zip file
|
||||
remainingServers, newWarnings, err := getRemainingServers(ctx, mapping, presolver)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
servers = append(servers, remainingServers...)
|
||||
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].Region < servers[j].Region
|
||||
})
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func getRemainingServers(ctx context.Context, mapping map[string]string, presolver resolver.Parallel) (
|
||||
servers []models.SurfsharkServer, warnings []string, err error) {
|
||||
hosts := make([]string, 0, len(mapping))
|
||||
for subdomain := range mapping {
|
||||
hosts = append(hosts, subdomain+".prod.surfshark.com")
|
||||
}
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.3
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
settings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
hostToIPs, warnings, err := presolver.Resolve(ctx, hosts, settings)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
servers = make([]models.SurfsharkServer, 0, len(hostToIPs))
|
||||
for host, IPs := range hostToIPs {
|
||||
subdomain := strings.TrimSuffix(host, ".prod.surfshark.com")
|
||||
server := models.SurfsharkServer{
|
||||
Region: mapping[subdomain],
|
||||
IPs: IPs,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func stringifySurfsharkServers(servers []models.SurfsharkServer) (s string) {
|
||||
s = "func SurfsharkServers() []models.SurfsharkServer {\n"
|
||||
s += " return []models.SurfsharkServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
|
||||
func surfsharkSubdomainToRegion() (mapping map[string]string) {
|
||||
return map[string]string{
|
||||
"ae-dub": "United Arab Emirates",
|
||||
"al-tia": "Albania",
|
||||
"at-vie": "Austria",
|
||||
"au-adl": "Australia Adelaide",
|
||||
"au-bne": "Australia Brisbane",
|
||||
"au-mel": "Australia Melbourne",
|
||||
"au-per": "Australia Perth",
|
||||
"au-syd": "Australia Sydney",
|
||||
"au-us": "Australia US",
|
||||
"az-bak": "Azerbaijan",
|
||||
"ba-sjj": "Bosnia and Herzegovina",
|
||||
"be-bru": "Belgium",
|
||||
"bg-sof": "Bulgaria",
|
||||
"br-sao": "Brazil",
|
||||
"ca-mon": "Canada Montreal",
|
||||
"ca-tor": "Canada Toronto",
|
||||
"ca-us": "Canada US",
|
||||
"ca-van": "Canada Vancouver",
|
||||
"ch-zur": "Switzerland",
|
||||
"cl-san": "Chile",
|
||||
"co-bog": "Colombia",
|
||||
"cr-sjn": "Costa Rica",
|
||||
"cy-nic": "Cyprus",
|
||||
"cz-prg": "Czech Republic",
|
||||
"de-ber": "Germany Berlin",
|
||||
"de-fra": "Germany Frankfurt am Main",
|
||||
"de-fra-st001": "Germany Frankfurt am Main st001",
|
||||
"de-fra-st002": "Germany Frankfurt am Main st002",
|
||||
"de-fra-st003": "Germany Frankfurt am Main st003",
|
||||
"de-fra-st004": "Germany Frankfurt am Main st004",
|
||||
"de-fra-st005": "Germany Frankfurt am Main st005",
|
||||
"de-muc": "Germany Munich",
|
||||
"de-nue": "Germany Nuremberg",
|
||||
"de-sg": "Germany Singapour",
|
||||
"de-uk": "Germany UK",
|
||||
"dk-cph": "Denmark",
|
||||
"ee-tll": "Estonia",
|
||||
"es-bcn": "Spain Barcelona",
|
||||
"es-mad": "Spain Madrid",
|
||||
"es-vlc": "Spain Valencia",
|
||||
"fi-hel": "Finland",
|
||||
"fr-bod": "France Bordeaux",
|
||||
"fr-mrs": "France Marseilles",
|
||||
"fr-par": "France Paris",
|
||||
"fr-se": "France Sweden",
|
||||
"gr-ath": "Greece",
|
||||
"hk-hkg": "Hong Kong",
|
||||
"hr-zag": "Croatia",
|
||||
"hu-bud": "Hungary",
|
||||
"id-jak": "Indonesia",
|
||||
"ie-dub": "Ireland",
|
||||
"il-tlv": "Israel",
|
||||
"in-chn": "India Chennai",
|
||||
"in-idr": "India Indore",
|
||||
"in-mum": "India Mumbai",
|
||||
"in-uk": "India UK",
|
||||
"is-rkv": "Iceland",
|
||||
"it-mil": "Italy Milan",
|
||||
"it-rom": "Italy Rome",
|
||||
"jp-tok": "Japan Tokyo",
|
||||
"jp-tok-st001": "Japan Tokyo st001",
|
||||
"jp-tok-st002": "Japan Tokyo st002",
|
||||
"jp-tok-st003": "Japan Tokyo st003",
|
||||
"jp-tok-st004": "Japan Tokyo st004",
|
||||
"jp-tok-st005": "Japan Tokyo st005",
|
||||
"jp-tok-st006": "Japan Tokyo st006",
|
||||
"jp-tok-st007": "Japan Tokyo st007",
|
||||
"jp-tok-st008": "Japan Tokyo st008",
|
||||
"jp-tok-st009": "Japan Tokyo st009",
|
||||
"jp-tok-st010": "Japan Tokyo st010",
|
||||
"jp-tok-st011": "Japan Tokyo st011",
|
||||
"jp-tok-st012": "Japan Tokyo st012",
|
||||
"jp-tok-st013": "Japan Tokyo st013",
|
||||
"kr-seo": "Korea",
|
||||
"kz-ura": "Kazakhstan",
|
||||
"lu-ste": "Luxembourg",
|
||||
"lv-rig": "Latvia",
|
||||
"ly-tip": "Libya",
|
||||
"md-chi": "Moldova",
|
||||
"mk-skp": "North Macedonia",
|
||||
"my-kul": "Malaysia",
|
||||
"ng-lag": "Nigeria",
|
||||
"nl-ams": "Netherlands Amsterdam",
|
||||
"nl-ams-st001": "Netherlands Amsterdam st001",
|
||||
"nl-us": "Netherlands US",
|
||||
"no-osl": "Norway",
|
||||
"nz-akl": "New Zealand",
|
||||
"ph-mnl": "Philippines",
|
||||
"pl-gdn": "Poland Gdansk",
|
||||
"pl-waw": "Poland Warsaw",
|
||||
"pt-lis": "Portugal Lisbon",
|
||||
"pt-lou": "Portugal Loule",
|
||||
"pt-opo": "Portugal Porto",
|
||||
"py-asu": "Paraguay",
|
||||
"ro-buc": "Romania",
|
||||
"rs-beg": "Serbia",
|
||||
"ru-mos": "Russia Moscow",
|
||||
"ru-spt": "Russia St. Petersburg",
|
||||
"se-sto": "Sweden",
|
||||
"sg-hk": "Singapore Hong Kong",
|
||||
"sg-nl": "Singapore Netherlands",
|
||||
"sg-sng": "Singapore",
|
||||
"sg-in": "Singapore in",
|
||||
"sg-sng-st001": "Singapore st001",
|
||||
"sg-sng-st002": "Singapore st002",
|
||||
"sg-sng-st003": "Singapore st003",
|
||||
"sg-sng-st004": "Singapore st004",
|
||||
"sg-sng-mp001": "Singapore mp001",
|
||||
"si-lju": "Slovenia",
|
||||
"sk-bts": "Slovekia",
|
||||
"th-bkk": "Thailand",
|
||||
"tr-bur": "Turkey",
|
||||
"tw-tai": "Taiwan",
|
||||
"ua-iev": "Ukraine",
|
||||
"uk-de": "UK Germany",
|
||||
"uk-fr": "UK France",
|
||||
"uk-gla": "UK Glasgow",
|
||||
"uk-lon": "UK London",
|
||||
"uk-lon-mp001": "UK London mp001",
|
||||
"uk-lon-st001": "UK London st001",
|
||||
"uk-lon-st002": "UK London st002",
|
||||
"uk-lon-st003": "UK London st003",
|
||||
"uk-lon-st004": "UK London st004",
|
||||
"uk-lon-st005": "UK London st005",
|
||||
"uk-man": "UK Manchester",
|
||||
"us-atl": "US Atlanta",
|
||||
"us-bdn": "US Bend",
|
||||
"us-bos": "US Boston",
|
||||
"us-buf": "US Buffalo",
|
||||
"us-chi": "US Chicago",
|
||||
"us-clt": "US Charlotte",
|
||||
"us-dal": "US Dallas",
|
||||
"us-den": "US Denver",
|
||||
"us-dtw": "US Gahanna",
|
||||
"us-hou": "US Houston",
|
||||
"us-kan": "US Kansas City",
|
||||
"us-las": "US Las Vegas",
|
||||
"us-lax": "US Los Angeles",
|
||||
"us-ltm": "US Latham",
|
||||
"us-mia": "US Miami",
|
||||
"us-mnz": "US Maryland",
|
||||
"us-nl": "US Netherlands",
|
||||
"us-nyc": "US New York City",
|
||||
"us-nyc-mp001": "US New York City mp001",
|
||||
"us-nyc-st001": "US New York City st001",
|
||||
"us-nyc-st002": "US New York City st002",
|
||||
"us-nyc-st003": "US New York City st003",
|
||||
"us-nyc-st004": "US New York City st004",
|
||||
"us-nyc-st005": "US New York City st005",
|
||||
"us-orl": "US Orlando",
|
||||
"us-phx": "US Phoenix",
|
||||
"us-pt": "US Portugal",
|
||||
"us-sea": "US Seatle",
|
||||
"us-sfo": "US San Francisco",
|
||||
"us-slc": "US Salt Lake City",
|
||||
"us-stl": "US Saint Louis",
|
||||
"us-tpa": "US Tampa",
|
||||
"vn-hcm": "Vietnam",
|
||||
"za-jnb": "South Africa",
|
||||
"ar-bua": "Argentina Buenos Aires",
|
||||
"tr-ist": "Turkey Istanbul",
|
||||
"mx-mex": "Mexico City Mexico",
|
||||
"ca-tor-mp001": "Canada Toronto mp001",
|
||||
"de-fra-mp001": "Germany Frankfurt mp001",
|
||||
"nl-ams-mp001": "Netherlands Amsterdam mp001",
|
||||
"us-sfo-mp001": "US San Francisco mp001",
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func (u *updater) updateTorguard(ctx context.Context) (err error) {
|
||||
servers, warnings, err := findTorguardServersFromZip(ctx, u.client)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Torguard: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update Torguard servers: %w", err)
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(stringifyTorguardServers(servers))
|
||||
}
|
||||
u.servers.Torguard.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Torguard.Servers = servers
|
||||
return nil
|
||||
}
|
||||
|
||||
func findTorguardServersFromZip(ctx context.Context, client *http.Client) (
|
||||
servers []models.TorguardServer, warnings []string, err error) {
|
||||
// Note: all servers do both TCP and UDP
|
||||
const zipURL = "https://torguard.net/downloads/OpenVPN-TCP-Linux.zip"
|
||||
|
||||
contents, err := fetchAndExtractFiles(ctx, client, zipURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for fileName, content := range contents {
|
||||
var server models.TorguardServer
|
||||
|
||||
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, ".")
|
||||
server.Country = parts[0]
|
||||
server.City = parts[1]
|
||||
case strings.HasPrefix(s, "USA"):
|
||||
server.Country = "USA"
|
||||
s = strings.TrimPrefix(s, "USA-")
|
||||
s = strings.ReplaceAll(s, "-", " ")
|
||||
s = strings.ReplaceAll(s, ".", " ")
|
||||
s = strings.ToLower(s)
|
||||
s = strings.Title(s)
|
||||
server.City = s
|
||||
default:
|
||||
server.Country = s
|
||||
}
|
||||
|
||||
hostnames := extractRemoteHostsFromOpenvpn(content, true, false)
|
||||
if len(hostnames) != 1 {
|
||||
warning := "found " + strconv.Itoa(len(hostnames)) +
|
||||
" hostname(s) instead of 1 in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
server.Hostname = hostnames[0]
|
||||
|
||||
IPs := extractRemoteHostsFromOpenvpn(content, false, true)
|
||||
if len(IPs) != 1 {
|
||||
warning := "found " + strconv.Itoa(len(IPs)) +
|
||||
" IP(s) instead of 1 in " + fileName
|
||||
warnings = append(warnings, warning)
|
||||
continue
|
||||
}
|
||||
server.IP = net.ParseIP(IPs[0])
|
||||
if server.IP == nil {
|
||||
warnings = append(warnings, "IP address "+IPs[0]+" is not valid in file "+fileName)
|
||||
}
|
||||
|
||||
servers = append(servers, 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
|
||||
})
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
|
||||
func stringifyTorguardServers(servers []models.TorguardServer) (s string) {
|
||||
s = "func TorguardServers() []models.TorguardServer {\n"
|
||||
s += " return []models.TorguardServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user