chore(all): memory and thread safe storage

- settings: get filter choices from storage for settings validation
- updater: update servers to the storage
- storage: minimal deep copying and data duplication
- storage: add merged servers mutex for thread safety
- connection: filter servers in storage
- formatter: format servers to Markdown in storage
- PIA: get server by name from storage directly
- Updater: get servers count from storage directly
- Updater: equality check done in storage, fix #882
This commit is contained in:
Quentin McGaw
2022-06-05 14:58:46 +00:00
parent 1e6b4ed5eb
commit 36b504609b
84 changed files with 1267 additions and 877 deletions

View File

@@ -0,0 +1,10 @@
package models
type FilterChoices struct {
Countries []string
Regions []string
Cities []string
ISPs []string
Names []string
Hostnames []string
}

View File

@@ -1,51 +0,0 @@
package models
import (
"net"
)
func (a AllServers) GetCopy() (allServersCopy AllServers) {
allServersCopy.Version = a.Version
allServersCopy.ProviderToServers = make(map[string]Servers, len(a.ProviderToServers))
for provider, servers := range a.ProviderToServers {
allServersCopy.ProviderToServers[provider] = Servers{
Version: servers.Version,
Timestamp: servers.Timestamp,
Servers: copyServers(servers.Servers),
}
}
return allServersCopy
}
func copyServers(servers []Server) (serversCopy []Server) {
if servers == nil {
return nil
}
serversCopy = make([]Server, len(servers))
for i, server := range servers {
serversCopy[i] = server
serversCopy[i].IPs = copyIPs(server.IPs)
}
return serversCopy
}
func copyIPs(toCopy []net.IP) (copied []net.IP) {
if toCopy == nil {
return nil
}
copied = make([]net.IP, len(toCopy))
for i := range toCopy {
copied[i] = copyIP(toCopy[i])
}
return copied
}
func copyIP(toCopy net.IP) (copied net.IP) {
copied = make(net.IP, len(toCopy))
copy(copied, toCopy)
return copied
}

View File

@@ -1,173 +0,0 @@
package models
import (
"net"
"testing"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_AllServers_GetCopy(t *testing.T) {
allServers := AllServers{
Version: 1,
ProviderToServers: map[string]Servers{
providers.Cyberghost: {
Version: 2,
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Expressvpn: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Fastestvpn: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.HideMyAss: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Ipvanish: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Ivpn: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Mullvad: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Nordvpn: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Perfectprivacy: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Privado: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.PrivateInternetAccess: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Privatevpn: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Protonvpn: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Purevpn: {
Version: 1,
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Surfshark: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Torguard: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.VPNUnlimited: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Vyprvpn: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Wevpn: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
providers.Windscribe: {
Servers: []Server{{
IPs: []net.IP{{1, 2, 3, 4}},
}},
},
},
}
servers := allServers.GetCopy()
assert.Equal(t, allServers, servers)
}
func Test_copyIPs(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
toCopy []net.IP
copied []net.IP
}{
"nil": {},
"empty": {
toCopy: []net.IP{},
copied: []net.IP{},
},
"single IP": {
toCopy: []net.IP{{1, 1, 1, 1}},
copied: []net.IP{{1, 1, 1, 1}},
},
"two IPs": {
toCopy: []net.IP{{1, 1, 1, 1}, {2, 2, 2, 2}},
copied: []net.IP{{1, 1, 1, 1}, {2, 2, 2, 2}},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
// Reserver leading 9 for copy modifications below
for _, ipToCopy := range testCase.toCopy {
require.NotEqual(t, 9, ipToCopy[0])
}
copied := copyIPs(testCase.toCopy)
assert.Equal(t, testCase.copied, copied)
if len(copied) > 0 {
original := testCase.toCopy[0][0]
testCase.toCopy[0][0] = 9
assert.NotEqual(t, 9, copied[0][0])
testCase.toCopy[0][0] = original
copied[0][0] = 9
assert.NotEqual(t, 9, testCase.toCopy[0][0])
}
})
}
}

View File

@@ -2,6 +2,7 @@ package models
import (
"net"
"reflect"
)
type Server struct {
@@ -26,3 +27,28 @@ type Server struct {
PortForward bool `json:"port_forward,omitempty"`
IPs []net.IP `json:"ips,omitempty"`
}
func (s *Server) Equal(other Server) (equal bool) {
if !ipsAreEqual(s.IPs, other.IPs) {
return false
}
serverCopy := *s
serverCopy.IPs = nil
other.IPs = nil
return reflect.DeepEqual(serverCopy, other)
}
func ipsAreEqual(a, b []net.IP) (equal bool) {
if len(a) != len(b) {
return false
}
for i := range a {
if !a[i].Equal(b[i]) {
return false
}
}
return true
}

View File

@@ -0,0 +1,120 @@
package models
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Server_Equal(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
a *Server
b Server
equal bool
}{
"same IPs": {
a: &Server{
IPs: []net.IP{net.IPv4(1, 2, 3, 4)},
},
b: Server{
IPs: []net.IP{net.IPv4(1, 2, 3, 4)},
},
equal: true,
},
"same IP strings": {
a: &Server{
IPs: []net.IP{net.IPv4(1, 2, 3, 4)},
},
b: Server{
IPs: []net.IP{{1, 2, 3, 4}},
},
equal: true,
},
"different IPs": {
a: &Server{
IPs: []net.IP{{1, 2, 3, 4}, {2, 3, 4, 5}},
},
b: Server{
IPs: []net.IP{{1, 2, 3, 4}, {1, 2, 3, 4}},
},
},
"all fields equal": {
a: &Server{
VPN: "vpn",
Country: "country",
Region: "region",
City: "city",
ISP: "isp",
Owned: true,
Number: 1,
ServerName: "server_name",
Hostname: "hostname",
TCP: true,
UDP: true,
OvpnX509: "x509",
RetroLoc: "retroloc",
MultiHop: true,
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
PortForward: true,
IPs: []net.IP{net.IPv4(1, 2, 3, 4)},
},
b: Server{
VPN: "vpn",
Country: "country",
Region: "region",
City: "city",
ISP: "isp",
Owned: true,
Number: 1,
ServerName: "server_name",
Hostname: "hostname",
TCP: true,
UDP: true,
OvpnX509: "x509",
RetroLoc: "retroloc",
MultiHop: true,
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
PortForward: true,
IPs: []net.IP{net.IPv4(1, 2, 3, 4)},
},
equal: true,
},
"different field": {
a: &Server{
VPN: "vpn",
},
b: Server{
VPN: "other vpn",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ipsOfANotNil := testCase.a.IPs != nil
ipsOfBNotNil := testCase.b.IPs != nil
equal := testCase.a.Equal(testCase.b)
assert.Equal(t, testCase.equal, equal)
// Ensure IPs field is not modified
if ipsOfANotNil {
assert.NotNil(t, testCase.a)
}
if ipsOfBNotNil {
assert.NotNil(t, testCase.b)
}
})
}
}

View File

@@ -15,18 +15,6 @@ type AllServers struct {
ProviderToServers map[string]Servers
}
func (a *AllServers) ServersSlice(provider string) []Server {
if provider == providers.Custom {
return nil
}
servers, ok := a.ProviderToServers[provider]
if !ok {
panic(fmt.Sprintf("provider %s not found in all servers", provider))
}
return copyServers(servers.Servers)
}
var _ json.Marshaler = (*AllServers)(nil)
// MarshalJSON marshals all servers to JSON.