Feat: rework Surfshark servers data (#575)

- Feat: `MULTIHOP_ONLY` variable
- Feat: `COUNTRY` variable
- Feat: `CITY` variable
- Feat: `REGION` variable, with retro-compatibility
- Feat: merge servers from API, zip and hardcoded hostnames
- Fix: remove outdated and duplicate servers
- Maint: faster update with fully parallel DNS resolutions
This commit is contained in:
Quentin McGaw
2021-08-23 10:25:00 -07:00
committed by GitHub
parent 8b52af0d03
commit f1a82d9d9c
21 changed files with 3023 additions and 1803 deletions

View File

@@ -6,21 +6,43 @@ import (
"errors"
"fmt"
"net/http"
"github.com/qdm12/gluetun/internal/constants"
)
// Note: no multi-hop and some servers are missing from their API.
func addServersFromAPI(ctx context.Context, client *http.Client,
hts hostToServer) (err error) {
data, err := fetchAPI(ctx, client)
if err != nil {
return err
}
hostToLocation := constants.SurfsharkHostToLocation()
const tcp, udp = true, true
for _, serverData := range data {
locationData := hostToLocation[serverData.Host] // TODO remove in v4
retroLoc := locationData.RetroLoc // empty string if the host has no retro-compatible region
hts.add(serverData.Host, serverData.Region, serverData.Country,
serverData.Location, retroLoc, tcp, udp)
}
return nil
}
var (
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
ErrUnmarshalResponseBody = errors.New("failed unmarshaling response body")
)
//nolint:unused
type serverData struct {
Host string `json:"connectionName"`
Region string `json:"region"`
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"
@@ -37,7 +59,8 @@ func fetchAPI(ctx context.Context, client *http.Client) (
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %s", ErrHTTPStatusCodeNotOK, response.Status)
return nil, fmt.Errorf("%w: %d %s", ErrHTTPStatusCodeNotOK,
response.StatusCode, response.Status)
}
decoder := json.NewDecoder(response.Body)

View File

@@ -0,0 +1,166 @@
package surfshark
import (
"context"
"errors"
"io"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_addServersFromAPI(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
hts hostToServer
responseStatus int
responseBody io.ReadCloser
expected hostToServer
err error
}{
"fetch API error": {
responseStatus: http.StatusNoContent,
err: errors.New("HTTP status code not OK: 204 No Content"),
},
"success": {
hts: hostToServer{
"existinghost": {Hostname: "existinghost"},
},
responseStatus: http.StatusOK,
responseBody: ioutil.NopCloser(strings.NewReader(`[
{"connectionName":"host1","region":"region1","country":"country1","location":"location1"},
{"connectionName":"host2","region":"region2","country":"country1","location":"location2"}
]`)),
expected: map[string]models.SurfsharkServer{
"existinghost": {Hostname: "existinghost"},
"host1": {
Region: "region1",
Country: "country1",
City: "location1",
Hostname: "host1",
TCP: true,
UDP: true,
},
"host2": {
Region: "region2",
Country: "country1",
City: "location2",
Hostname: "host2",
TCP: true,
UDP: true,
}},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, r.URL.String(), "https://my.surfshark.com/vpn/api/v4/server/clusters")
return &http.Response{
StatusCode: testCase.responseStatus,
Status: http.StatusText(testCase.responseStatus),
Body: testCase.responseBody,
}, nil
}),
}
err := addServersFromAPI(ctx, client, testCase.hts)
assert.Equal(t, testCase.expected, testCase.hts)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
func Test_fetchAPI(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
responseStatus int
responseBody io.ReadCloser
data []serverData
err error
}{
"http response status not ok": {
responseStatus: http.StatusNoContent,
err: errors.New("HTTP status code not OK: 204 No Content"),
},
"nil body": {
responseStatus: http.StatusOK,
err: errors.New("failed unmarshaling response body: EOF"),
},
"no server": {
responseStatus: http.StatusOK,
responseBody: ioutil.NopCloser(strings.NewReader(`[]`)),
data: []serverData{},
},
"success": {
responseStatus: http.StatusOK,
responseBody: ioutil.NopCloser(strings.NewReader(`[
{"connectionName":"host1","region":"region1","country":"country1","location":"location1"},
{"connectionName":"host2","region":"region2","country":"country1","location":"location2"}
]`)),
data: []serverData{
{
Region: "region1",
Country: "country1",
Location: "location1",
Host: "host1",
},
{
Region: "region2",
Country: "country1",
Location: "location2",
Host: "host2",
},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, r.URL.String(), "https://my.surfshark.com/vpn/api/v4/server/clusters")
return &http.Response{
StatusCode: testCase.responseStatus,
Status: http.StatusText(testCase.responseStatus),
Body: testCase.responseBody,
}, nil
}),
}
data, err := fetchAPI(ctx, client)
assert.Equal(t, testCase.data, data)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -2,18 +2,20 @@ package surfshark
import (
"net"
"strings"
"github.com/qdm12/gluetun/internal/models"
)
type hostToServer map[string]models.SurfsharkServer
func (hts hostToServer) add(host, region string, tcp, udp bool) {
func (hts hostToServer) add(host, region, country, city, retroLoc string, tcp, udp bool) {
server, ok := hts[host]
if !ok {
server.Hostname = host
server.Region = region
server.Country = country
server.City = city
server.RetroLoc = retroLoc
}
if tcp {
server.TCP = tcp
@@ -32,16 +34,6 @@ func (hts hostToServer) toHostsSlice() (hosts []string) {
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]

View File

@@ -0,0 +1,22 @@
package surfshark
import (
"errors"
"fmt"
"github.com/qdm12/gluetun/internal/models"
)
var (
errHostnameNotFound = errors.New("hostname not found in hostname to location mapping")
)
func getHostInformation(host string, hostnameToLocation map[string]models.SurfsharkLocationData) (
data models.SurfsharkLocationData, err error) {
locationData, ok := hostnameToLocation[host]
if !ok {
return locationData, fmt.Errorf("%w: %s", errHostnameNotFound, host)
}
return locationData, nil
}

View File

@@ -1,200 +0,0 @@
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",
}
}

View File

@@ -0,0 +1,20 @@
package surfshark
import (
"github.com/qdm12/gluetun/internal/constants"
)
// getRemainingServers finds extra servers not found in the API or in the ZIP file.
func getRemainingServers(hts hostToServer) {
hostnameToLocationLeft := constants.SurfsharkHostToLocation()
for _, hostnameDone := range hts.toHostsSlice() {
delete(hostnameToLocationLeft, hostnameDone)
}
for hostname, locationData := range hostnameToLocationLeft {
// we assume the server supports TCP and UDP
const tcp, udp = true, true
hts.add(hostname, locationData.Region, locationData.Country,
locationData.City, locationData.RetroLoc, tcp, udp)
}
}

View File

@@ -0,0 +1,57 @@
package surfshark
import (
"context"
"errors"
"net"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
"github.com/stretchr/testify/assert"
)
func Test_resolveHosts(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := context.Background()
presolver := mock_resolver.NewMockParallel(ctrl)
hosts := []string{"host1", "host2"}
const minServers = 10
expectedHostToIPs := map[string][]net.IP{
"host1": {{1, 2, 3, 4}},
"host2": {{2, 3, 4, 5}},
}
expectedWarnings := []string{"warning1", "warning2"}
expectedErr := errors.New("dummy")
const (
maxFailRatio = 0.1
maxDuration = 20 * time.Second
betweenDuration = time.Second
maxNoNew = 2
maxFails = 2
)
expectedSettings := resolver.ParallelSettings{
MaxFailRatio: maxFailRatio,
MinFound: minServers,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
BetweenDuration: betweenDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
SortIPs: true,
},
}
presolver.EXPECT().Resolve(ctx, hosts, expectedSettings).
Return(expectedHostToIPs, expectedWarnings, expectedErr)
hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers)
assert.Equal(t, expectedHostToIPs, hostToIPs)
assert.Equal(t, expectedWarnings, warnings)
assert.Equal(t, expectedErr, err)
}

View File

@@ -0,0 +1,9 @@
package surfshark
import "net/http"
type roundTripFunc func(r *http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return f(r)
}

View File

@@ -6,71 +6,36 @@ import (
"context"
"errors"
"fmt"
"strings"
"net/http"
"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")
var (
ErrGetZip = errors.New("cannot get OpenVPN ZIP file")
ErrGetAPI = errors.New("cannot fetch server information from API")
ErrNotEnoughServers = errors.New("not enough servers found")
)
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
presolver resolver.Parallel, minServers int) (
client *http.Client, 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
}
tcp, udp, err := openvpn.ExtractProto(content)
if err != nil {
// treat error as warning and go to next file
warning := err.Error() + " in " + fileName
warnings = append(warnings, warning)
continue
}
host, warning, err := openvpn.ExtractHost(content)
if warning != "" {
warnings = append(warnings, warning)
}
if err != nil {
// treat error as warning and go to next file
warning := err.Error() + " in " + fileName
warnings = append(warnings, warning)
continue
}
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, tcp, udp)
err = addServersFromAPI(ctx, client, hts)
if err != nil {
return nil, nil, fmt.Errorf("%w: %s", ErrGetAPI, err)
}
if len(hts) < minServers {
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
ErrNotEnoughServers, len(hts), minServers)
warnings, err = addOpenVPNServersFromZip(ctx, unzipper, hts)
if err != nil {
return nil, nil, fmt.Errorf("%w: %s", ErrGetZip, err)
}
getRemainingServers(hts)
hosts := hts.toHostsSlice()
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
warnings = append(warnings, newWarnings...)
@@ -82,20 +47,6 @@ func GetServers(ctx context.Context, unzipper unzip.Unzipper,
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)
@@ -105,39 +56,3 @@ func GetServers(ctx context.Context, unzipper unzip.Unzipper,
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
}
// we assume the server supports TCP and UDP
server := models.SurfsharkServer{
Region: region,
Hostname: host,
TCP: true,
UDP: true,
IPs: IPs,
}
servers = append(servers, server)
}
return servers, warnings, nil
}

View File

@@ -9,7 +9,13 @@ import (
func sortServers(servers []models.SurfsharkServer) {
sort.Slice(servers, func(i, j int) bool {
if servers[i].Region == servers[j].Region {
return servers[i].Hostname < servers[j].Hostname
if servers[i].Country == servers[j].Country {
if servers[i].City == servers[j].City {
return servers[i].Hostname < servers[j].Hostname
}
return servers[i].City < servers[j].City
}
return servers[i].Country < servers[j].Country
}
return servers[i].Region < servers[j].Region
})

View File

@@ -0,0 +1,42 @@
package surfshark
import (
"testing"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
)
func Test_sortServers(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
initialServers []models.SurfsharkServer
sortedServers []models.SurfsharkServer
}{
"no server": {},
"sorted servers": {
initialServers: []models.SurfsharkServer{
{Region: "Z", Country: "B", City: "A", Hostname: "A"},
{Country: "B", City: "A", Hostname: "A"},
{Country: "A", City: "A", Hostname: "B"},
{Country: "A", City: "A", Hostname: "A"},
{Country: "A", City: "B", Hostname: "A"},
},
sortedServers: []models.SurfsharkServer{
{Country: "A", City: "A", Hostname: "A"},
{Country: "A", City: "A", Hostname: "B"},
{Country: "A", City: "B", Hostname: "A"},
{Country: "B", City: "A", Hostname: "A"},
{Region: "Z", Country: "B", City: "A", Hostname: "A"},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
sortServers(testCase.initialServers)
assert.Equal(t, testCase.sortedServers, testCase.initialServers)
})
}
}

View File

@@ -0,0 +1,71 @@
package surfshark
import (
"context"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/updater/openvpn"
"github.com/qdm12/gluetun/internal/updater/unzip"
)
func addOpenVPNServersFromZip(ctx context.Context,
unzipper unzip.Unzipper, hts hostToServer) (
warnings []string, err error) {
const url = "https://my.surfshark.com/vpn/api/v1/server/configurations"
contents, err := unzipper.FetchAndExtract(ctx, url)
if err != nil {
return nil, err
}
hostnamesDone := hts.toHostsSlice()
hostnamesDoneSet := make(map[string]struct{}, len(hostnamesDone))
for _, hostname := range hostnamesDone {
hostnamesDoneSet[hostname] = struct{}{}
}
hostToLocation := constants.SurfsharkHostToLocation()
for fileName, content := range contents {
if !strings.HasSuffix(fileName, ".ovpn") {
continue // not an OpenVPN file
}
host, warning, err := openvpn.ExtractHost(content)
if warning != "" {
warnings = append(warnings, warning)
}
if err != nil {
// treat error as warning and go to next file
warning := err.Error() + " in " + fileName
warnings = append(warnings, warning)
continue
}
_, ok := hostnamesDoneSet[host]
if ok {
continue // already done in API
}
tcp, udp, err := openvpn.ExtractProto(content)
if err != nil {
// treat error as warning and go to next file
warning := err.Error() + " in " + fileName
warnings = append(warnings, warning)
continue
}
data, err := getHostInformation(host, hostToLocation)
if err != nil {
// treat error as warning and go to next file
warning := err.Error()
warnings = append(warnings, warning)
continue
}
hts.add(host, data.Region, data.Country, data.City,
data.RetroLoc, tcp, udp)
}
return warnings, nil
}