Feature: IVPN support
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"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/ivpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/mullvad"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/nordvpn"
|
||||
"github.com/qdm12/gluetun/internal/updater/providers/pia"
|
||||
@@ -74,6 +75,26 @@ func (u *updater) updateHideMyAss(ctx context.Context) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) updateIvpn(ctx context.Context) (err error) {
|
||||
minServers := getMinServers(len(u.servers.Ivpn.Servers))
|
||||
servers, warnings, err := ivpn.GetServers(
|
||||
ctx, u.unzipper, u.presolver, minServers)
|
||||
if u.options.CLI {
|
||||
for _, warning := range warnings {
|
||||
u.logger.Warn("Ivpn: %s", warning)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.options.Stdout {
|
||||
u.println(ivpn.Stringify(servers))
|
||||
}
|
||||
u.servers.Ivpn.Timestamp = u.timeNow().Unix()
|
||||
u.servers.Ivpn.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)
|
||||
|
||||
16
internal/updater/providers/ivpn/filename.go
Normal file
16
internal/updater/providers/ivpn/filename.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseFilename(fileName string) (country, city string) {
|
||||
const suffix = ".ovpn"
|
||||
fileName = strings.TrimSuffix(fileName, suffix)
|
||||
parts := strings.Split(fileName, "-")
|
||||
country = strings.ReplaceAll(parts[0], "_", " ")
|
||||
if len(parts) > 1 {
|
||||
city = strings.ReplaceAll(parts[1], "_", " ")
|
||||
}
|
||||
return country, city
|
||||
}
|
||||
41
internal/updater/providers/ivpn/filename_test.go
Normal file
41
internal/updater/providers/ivpn/filename_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseFilename(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
fileName string
|
||||
country string
|
||||
city string
|
||||
}{
|
||||
"empty filename": {},
|
||||
"country only": {
|
||||
fileName: "Country.ovpn",
|
||||
country: "Country",
|
||||
},
|
||||
"country and city": {
|
||||
fileName: "Country-City.ovpn",
|
||||
country: "Country",
|
||||
city: "City",
|
||||
},
|
||||
"composite country and city": {
|
||||
fileName: "Coun_try-Ci_ty.ovpn",
|
||||
country: "Coun try",
|
||||
city: "Ci ty",
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
country, city := parseFilename(testCase.fileName)
|
||||
assert.Equal(t, testCase.country, country)
|
||||
assert.Equal(t, testCase.city, city)
|
||||
})
|
||||
}
|
||||
}
|
||||
58
internal/updater/providers/ivpn/hosttoserver.go
Normal file
58
internal/updater/providers/ivpn/hosttoserver.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.IvpnServer
|
||||
|
||||
func (hts hostToServer) add(host, country, city string, tcp, udp bool) {
|
||||
server, ok := hts[host]
|
||||
if !ok {
|
||||
server.Hostname = host
|
||||
server.Country = country
|
||||
server.City = city
|
||||
}
|
||||
if tcp {
|
||||
server.TCP = tcp
|
||||
}
|
||||
if udp {
|
||||
server.UDP = udp
|
||||
}
|
||||
hts[host] = server
|
||||
}
|
||||
|
||||
func (hts hostToServer) toHostsSlice() (hosts []string) {
|
||||
hosts = make([]string, 0, len(hts))
|
||||
for host := range hts {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
sort.Slice(hosts, func(i, j int) bool {
|
||||
return hosts[i] < hosts[j]
|
||||
})
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]net.IP) {
|
||||
for host, IPs := range hostToIPs {
|
||||
server := hts[host]
|
||||
server.IPs = IPs
|
||||
hts[host] = server
|
||||
}
|
||||
for host, server := range hts {
|
||||
if len(server.IPs) == 0 {
|
||||
delete(hts, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (hts hostToServer) toServersSlice() (servers []models.IvpnServer) {
|
||||
servers = make([]models.IvpnServer, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
211
internal/updater/providers/ivpn/hosttoserver_test.go
Normal file
211
internal/updater/providers/ivpn/hosttoserver_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_hostToServer_add(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
initialHTS hostToServer
|
||||
host string
|
||||
country string
|
||||
city string
|
||||
tcp bool
|
||||
udp bool
|
||||
expectedHTS hostToServer
|
||||
}{
|
||||
"empty host to server": {
|
||||
initialHTS: hostToServer{},
|
||||
host: "host",
|
||||
country: "country",
|
||||
city: "city",
|
||||
tcp: true,
|
||||
udp: true,
|
||||
expectedHTS: hostToServer{
|
||||
"host": {
|
||||
Hostname: "host",
|
||||
Country: "country",
|
||||
City: "city",
|
||||
TCP: true,
|
||||
UDP: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"add server": {
|
||||
initialHTS: hostToServer{
|
||||
"existing host": {},
|
||||
},
|
||||
host: "host",
|
||||
country: "country",
|
||||
city: "city",
|
||||
tcp: true,
|
||||
udp: true,
|
||||
expectedHTS: hostToServer{
|
||||
"existing host": {},
|
||||
"host": models.IvpnServer{
|
||||
Hostname: "host",
|
||||
Country: "country",
|
||||
City: "city",
|
||||
TCP: true,
|
||||
UDP: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"extend existing server": {
|
||||
initialHTS: hostToServer{
|
||||
"host": models.IvpnServer{
|
||||
Hostname: "host",
|
||||
Country: "country",
|
||||
City: "city",
|
||||
TCP: true,
|
||||
},
|
||||
},
|
||||
host: "host",
|
||||
country: "country",
|
||||
city: "city",
|
||||
tcp: false,
|
||||
udp: true,
|
||||
expectedHTS: hostToServer{
|
||||
"host": models.IvpnServer{
|
||||
Hostname: "host",
|
||||
Country: "country",
|
||||
City: "city",
|
||||
TCP: true,
|
||||
UDP: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCase.initialHTS.add(testCase.host, testCase.country, testCase.city, testCase.tcp, testCase.udp)
|
||||
assert.Equal(t, testCase.expectedHTS, testCase.initialHTS)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_hostToServer_toHostsSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
hts hostToServer
|
||||
hosts []string
|
||||
}{
|
||||
"empty host to server": {
|
||||
hts: hostToServer{},
|
||||
hosts: []string{},
|
||||
},
|
||||
"single host": {
|
||||
hts: hostToServer{
|
||||
"A": {},
|
||||
},
|
||||
hosts: []string{"A"},
|
||||
},
|
||||
"multiple hosts": {
|
||||
hts: hostToServer{
|
||||
"A": {},
|
||||
"B": {},
|
||||
},
|
||||
hosts: []string{"A", "B"},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
hosts := testCase.hts.toHostsSlice()
|
||||
assert.ElementsMatch(t, testCase.hosts, hosts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_hostToServer_adaptWithIPs(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
initialHTS hostToServer
|
||||
hostToIPs map[string][]net.IP
|
||||
expectedHTS hostToServer
|
||||
}{
|
||||
"create server": {
|
||||
initialHTS: hostToServer{},
|
||||
hostToIPs: map[string][]net.IP{
|
||||
"A": {{1, 2, 3, 4}},
|
||||
},
|
||||
expectedHTS: hostToServer{
|
||||
"A": models.IvpnServer{
|
||||
IPs: []net.IP{{1, 2, 3, 4}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"add IPs to existing server": {
|
||||
initialHTS: hostToServer{
|
||||
"A": models.IvpnServer{
|
||||
Country: "country",
|
||||
},
|
||||
},
|
||||
hostToIPs: map[string][]net.IP{
|
||||
"A": {{1, 2, 3, 4}},
|
||||
},
|
||||
expectedHTS: hostToServer{
|
||||
"A": models.IvpnServer{
|
||||
Country: "country",
|
||||
IPs: []net.IP{{1, 2, 3, 4}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"remove server without IP": {
|
||||
initialHTS: hostToServer{
|
||||
"A": models.IvpnServer{
|
||||
Country: "country",
|
||||
},
|
||||
},
|
||||
hostToIPs: map[string][]net.IP{},
|
||||
expectedHTS: hostToServer{},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCase.initialHTS.adaptWithIPs(testCase.hostToIPs)
|
||||
assert.Equal(t, testCase.expectedHTS, testCase.initialHTS)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_hostToServer_toServersSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
hts hostToServer
|
||||
servers []models.IvpnServer
|
||||
}{
|
||||
"empty host to server": {
|
||||
hts: hostToServer{},
|
||||
servers: []models.IvpnServer{},
|
||||
},
|
||||
"multiple servers": {
|
||||
hts: hostToServer{
|
||||
"A": {Country: "A"},
|
||||
"B": {Country: "B"},
|
||||
},
|
||||
servers: []models.IvpnServer{
|
||||
{Country: "A"},
|
||||
{Country: "B"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
servers := testCase.hts.toServersSlice()
|
||||
assert.ElementsMatch(t, testCase.servers, servers)
|
||||
})
|
||||
}
|
||||
}
|
||||
36
internal/updater/providers/ivpn/resolve.go
Normal file
36
internal/updater/providers/ivpn/resolve.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func getResolveSettings(minServers int) (settings resolver.ParallelSettings) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
return resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
|
||||
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
|
||||
warnings []string, err error) {
|
||||
settings := getResolveSettings(minServers)
|
||||
return presolver.Resolve(ctx, hosts, settings)
|
||||
}
|
||||
56
internal/updater/providers/ivpn/resolve_test.go
Normal file
56
internal/updater/providers/ivpn/resolve_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_resolveHosts(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
ctx := context.Background()
|
||||
presolver := mock_resolver.NewMockParallel(ctrl)
|
||||
hosts := []string{"host1", "host2"}
|
||||
const minServers = 10
|
||||
|
||||
expectedHostToIPs := map[string][]net.IP{
|
||||
"host1": {{1, 2, 3, 4}},
|
||||
"host2": {{2, 3, 4, 5}},
|
||||
}
|
||||
expectedWarnings := []string{"warning1", "warning2"}
|
||||
expectedErr := errors.New("dummy")
|
||||
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
expectedSettings := resolver.ParallelSettings{
|
||||
MaxFailRatio: maxFailRatio,
|
||||
MinFound: minServers,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
86
internal/updater/providers/ivpn/servers.go
Normal file
86
internal/updater/providers/ivpn/servers.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Package ivpn contains code to obtain the server information
|
||||
// for the Surshark provider.
|
||||
package ivpn
|
||||
|
||||
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.IvpnServer, warnings []string, err error) {
|
||||
const url = "https://www.ivpn.net/releases/config/ivpn-openvpn-config.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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
country, city := parseFilename(fileName)
|
||||
|
||||
hts.add(host, country, city, tcp, udp)
|
||||
}
|
||||
|
||||
if len(hts) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(hts), minServers)
|
||||
}
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
|
||||
if len(servers) < minServers {
|
||||
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
|
||||
ErrNotEnoughServers, len(servers), minServers)
|
||||
}
|
||||
|
||||
sortServers(servers)
|
||||
|
||||
return servers, warnings, nil
|
||||
}
|
||||
142
internal/updater/providers/ivpn/servers_test.go
Normal file
142
internal/updater/providers/ivpn/servers_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip/mock_unzip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
// Inputs
|
||||
minServers int
|
||||
|
||||
// Unzip
|
||||
unzipContents map[string][]byte
|
||||
unzipErr error
|
||||
|
||||
// Resolution
|
||||
expectResolve bool
|
||||
hostsToResolve []string
|
||||
resolveSettings resolver.ParallelSettings
|
||||
hostToIPs map[string][]net.IP
|
||||
resolveWarnings []string
|
||||
resolveErr error
|
||||
|
||||
// Output
|
||||
servers []models.IvpnServer
|
||||
warnings []string
|
||||
err error
|
||||
}{
|
||||
"unzipper error": {
|
||||
unzipErr: errors.New("dummy"),
|
||||
err: errors.New("dummy"),
|
||||
},
|
||||
"not enough unzip contents": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"no openvpn file": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{"somefile.txt": {}},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"invalid proto": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{"badproto.ovpn": []byte(`proto invalid`)},
|
||||
warnings: []string{"unknown protocol: invalid: in badproto.ovpn"},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"no host": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{"nohost.ovpn": []byte(``)},
|
||||
warnings: []string{"remote host not found in nohost.ovpn"},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"multiple hosts": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{
|
||||
"MultiHosts.ovpn": []byte("remote hosta\nremote hostb"),
|
||||
},
|
||||
expectResolve: true,
|
||||
hostsToResolve: []string{"hosta"},
|
||||
resolveSettings: getResolveSettings(1),
|
||||
warnings: []string{"only using the first host \"hosta\" and discarding 1 other hosts"},
|
||||
err: errors.New("not enough servers found: 0 and expected at least 1"),
|
||||
},
|
||||
"resolve error": {
|
||||
unzipContents: map[string][]byte{
|
||||
"config.ovpn": []byte("remote hosta"),
|
||||
},
|
||||
expectResolve: true,
|
||||
hostsToResolve: []string{"hosta"},
|
||||
resolveSettings: getResolveSettings(0),
|
||||
resolveWarnings: []string{"resolve warning"},
|
||||
resolveErr: errors.New("dummy"),
|
||||
warnings: []string{"resolve warning"},
|
||||
err: errors.New("dummy"),
|
||||
},
|
||||
"success": {
|
||||
minServers: 1,
|
||||
unzipContents: map[string][]byte{
|
||||
"Country1-City_A.ovpn": []byte("remote hosta"),
|
||||
"Country2-City_B.ovpn": []byte("remote hostb"),
|
||||
},
|
||||
expectResolve: true,
|
||||
hostsToResolve: []string{"hosta", "hostb"},
|
||||
resolveSettings: getResolveSettings(1),
|
||||
hostToIPs: map[string][]net.IP{
|
||||
"hosta": {{1, 1, 1, 1}, {2, 2, 2, 2}},
|
||||
"hostb": {{3, 3, 3, 3}, {4, 4, 4, 4}},
|
||||
},
|
||||
resolveWarnings: []string{"resolve warning"},
|
||||
servers: []models.IvpnServer{
|
||||
{Country: "Country1", City: "City A", Hostname: "hosta", UDP: true, IPs: []net.IP{{1, 1, 1, 1}, {2, 2, 2, 2}}},
|
||||
{Country: "Country2", City: "City B", Hostname: "hostb", UDP: true, IPs: []net.IP{{3, 3, 3, 3}, {4, 4, 4, 4}}},
|
||||
},
|
||||
warnings: []string{"resolve warning"},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
unzipper := mock_unzip.NewMockUnzipper(ctrl)
|
||||
const zipURL = "https://www.ivpn.net/releases/config/ivpn-openvpn-config.zip"
|
||||
unzipper.EXPECT().FetchAndExtract(ctx, zipURL).
|
||||
Return(testCase.unzipContents, testCase.unzipErr)
|
||||
|
||||
presolver := mock_resolver.NewMockParallel(ctrl)
|
||||
if testCase.expectResolve {
|
||||
presolver.EXPECT().Resolve(ctx, testCase.hostsToResolve, testCase.resolveSettings).
|
||||
Return(testCase.hostToIPs, testCase.resolveWarnings, testCase.resolveErr)
|
||||
}
|
||||
|
||||
servers, warnings, err := GetServers(ctx, unzipper, presolver, testCase.minServers)
|
||||
|
||||
assert.Equal(t, testCase.servers, servers)
|
||||
assert.Equal(t, testCase.warnings, warnings)
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
19
internal/updater/providers/ivpn/sort.go
Normal file
19
internal/updater/providers/ivpn/sort.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func sortServers(servers []models.IvpnServer) {
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
if servers[i].Country == servers[j].Country {
|
||||
if servers[i].City == servers[j].City {
|
||||
return servers[i].Hostname < servers[j].Hostname
|
||||
}
|
||||
return servers[i].City < servers[j].City
|
||||
}
|
||||
return servers[i].Country < servers[j].Country
|
||||
})
|
||||
}
|
||||
40
internal/updater/providers/ivpn/sort_test.go
Normal file
40
internal/updater/providers/ivpn/sort_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_sortServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
initialServers []models.IvpnServer
|
||||
sortedServers []models.IvpnServer
|
||||
}{
|
||||
"no server": {},
|
||||
"sorted servers": {
|
||||
initialServers: []models.IvpnServer{
|
||||
{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.IvpnServer{
|
||||
{Country: "A", City: "A", Hostname: "A"},
|
||||
{Country: "A", City: "A", Hostname: "B"},
|
||||
{Country: "A", City: "B", Hostname: "A"},
|
||||
{Country: "B", City: "A", Hostname: "A"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sortServers(testCase.initialServers)
|
||||
assert.Equal(t, testCase.sortedServers, testCase.initialServers)
|
||||
})
|
||||
}
|
||||
}
|
||||
14
internal/updater/providers/ivpn/string.go
Normal file
14
internal/updater/providers/ivpn/string.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package ivpn
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/models"
|
||||
|
||||
func Stringify(servers []models.IvpnServer) (s string) {
|
||||
s = "func IvpnServers() []models.IvpnServer {\n"
|
||||
s += " return []models.IvpnServer{\n"
|
||||
for _, server := range servers {
|
||||
s += " " + server.String() + ",\n"
|
||||
}
|
||||
s += " }\n"
|
||||
s += "}"
|
||||
return s
|
||||
}
|
||||
43
internal/updater/providers/ivpn/string_test.go
Normal file
43
internal/updater/providers/ivpn/string_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package ivpn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Stringify(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
servers []models.IvpnServer
|
||||
s string
|
||||
}{
|
||||
"no server": {
|
||||
s: `func IvpnServers() []models.IvpnServer {
|
||||
return []models.IvpnServer{
|
||||
}
|
||||
}`,
|
||||
},
|
||||
"multiple servers": {
|
||||
servers: []models.IvpnServer{
|
||||
{Country: "A"},
|
||||
{Country: "B"},
|
||||
},
|
||||
s: `func IvpnServers() []models.IvpnServer {
|
||||
return []models.IvpnServer{
|
||||
{Country: "A", City: "", Hostname: "", IPs: []net.IP{}},
|
||||
{Country: "B", City: "", Hostname: "", IPs: []net.IP{}},
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := Stringify(testCase.servers)
|
||||
assert.Equal(t, testCase.s, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
53
internal/updater/resolver/mock_resolver/parallel.go
Normal file
53
internal/updater/resolver/mock_resolver/parallel.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/updater/resolver (interfaces: Parallel)
|
||||
|
||||
// Package mock_resolver is a generated GoMock package.
|
||||
package mock_resolver
|
||||
|
||||
import (
|
||||
context "context"
|
||||
net "net"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
resolver "github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
// MockParallel is a mock of Parallel interface.
|
||||
type MockParallel struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockParallelMockRecorder
|
||||
}
|
||||
|
||||
// MockParallelMockRecorder is the mock recorder for MockParallel.
|
||||
type MockParallelMockRecorder struct {
|
||||
mock *MockParallel
|
||||
}
|
||||
|
||||
// NewMockParallel creates a new mock instance.
|
||||
func NewMockParallel(ctrl *gomock.Controller) *MockParallel {
|
||||
mock := &MockParallel{ctrl: ctrl}
|
||||
mock.recorder = &MockParallelMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockParallel) EXPECT() *MockParallelMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Resolve mocks base method.
|
||||
func (m *MockParallel) Resolve(arg0 context.Context, arg1 []string, arg2 resolver.ParallelSettings) (map[string][]net.IP, []string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Resolve", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(map[string][]net.IP)
|
||||
ret1, _ := ret[1].([]string)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// Resolve indicates an expected call of Resolve.
|
||||
func (mr *MockParallelMockRecorder) Resolve(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockParallel)(nil).Resolve), arg0, arg1, arg2)
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination=mock_$GOPACKAGE/$GOFILE . Parallel
|
||||
|
||||
type Parallel interface {
|
||||
Resolve(ctx context.Context, hosts []string, settings ParallelSettings) (
|
||||
hostToIPs map[string][]net.IP, warnings []string, err error)
|
||||
|
||||
50
internal/updater/unzip/mock_unzip/unzip.go
Normal file
50
internal/updater/unzip/mock_unzip/unzip.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/updater/unzip (interfaces: Unzipper)
|
||||
|
||||
// Package mock_unzip is a generated GoMock package.
|
||||
package mock_unzip
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockUnzipper is a mock of Unzipper interface.
|
||||
type MockUnzipper struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockUnzipperMockRecorder
|
||||
}
|
||||
|
||||
// MockUnzipperMockRecorder is the mock recorder for MockUnzipper.
|
||||
type MockUnzipperMockRecorder struct {
|
||||
mock *MockUnzipper
|
||||
}
|
||||
|
||||
// NewMockUnzipper creates a new mock instance.
|
||||
func NewMockUnzipper(ctrl *gomock.Controller) *MockUnzipper {
|
||||
mock := &MockUnzipper{ctrl: ctrl}
|
||||
mock.recorder = &MockUnzipperMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockUnzipper) EXPECT() *MockUnzipperMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// FetchAndExtract mocks base method.
|
||||
func (m *MockUnzipper) FetchAndExtract(arg0 context.Context, arg1 string) (map[string][]byte, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FetchAndExtract", arg0, arg1)
|
||||
ret0, _ := ret[0].(map[string][]byte)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FetchAndExtract indicates an expected call of FetchAndExtract.
|
||||
func (mr *MockUnzipperMockRecorder) FetchAndExtract(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAndExtract", reflect.TypeOf((*MockUnzipper)(nil).FetchAndExtract), arg0, arg1)
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination=mock_$GOPACKAGE/$GOFILE . Unzipper
|
||||
|
||||
type Unzipper interface {
|
||||
FetchAndExtract(ctx context.Context, url string) (contents map[string][]byte, err error)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,16 @@ func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServe
|
||||
}
|
||||
}
|
||||
|
||||
if u.options.Ivpn {
|
||||
u.logger.Info("updating Ivpn servers...")
|
||||
if err := u.updateIvpn(ctx); err != nil {
|
||||
u.logger.Error(err)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return allServers, err
|
||||
}
|
||||
}
|
||||
|
||||
if u.options.Mullvad {
|
||||
u.logger.Info("updating Mullvad servers...")
|
||||
if err := u.updateMullvad(ctx); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user