IVPN server data update code and ISP filter (#578)

- Use IVPN's HTTP API instead of their .zip file
- Unit tests for API and GetServers
- Paves the way for Wireguard
- Update server information for IVPN
- Add `ISP` filter for IVPN
This commit is contained in:
Quentin McGaw
2021-08-22 20:11:56 -07:00
committed by GitHub
parent b69dcb62e3
commit c348343b22
17 changed files with 711 additions and 614 deletions

View File

@@ -24,6 +24,11 @@ func (settings *Provider) readIvpn(r reader) (err error) {
return fmt.Errorf("environment variable CITY: %w", err)
}
settings.ServerSelection.ISPs, err = r.env.CSVInside("ISP", constants.IvpnISPChoices())
if err != nil {
return fmt.Errorf("environment variable ISP: %w", err)
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices())
if err != nil {
return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err)

View File

@@ -34,6 +34,7 @@ func Test_Provider_readIvpn(t *testing.T) {
targetIP singleStringCall
countries sliceStringCall
cities sliceStringCall
isps sliceStringCall
hostnames sliceStringCall
settings Provider
err error
@@ -62,10 +63,21 @@ func Test_Provider_readIvpn(t *testing.T) {
},
err: errors.New("environment variable CITY: dummy test error"),
},
"isps error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errors.New("environment variable ISP: dummy test error"),
},
"hostnames error": {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
@@ -76,6 +88,7 @@ func Test_Provider_readIvpn(t *testing.T) {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
protocol: singleStringCall{call: true, err: errDummy},
settings: Provider{
@@ -87,6 +100,7 @@ func Test_Provider_readIvpn(t *testing.T) {
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
isps: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
protocol: singleStringCall{call: true},
settings: Provider{
@@ -97,6 +111,7 @@ func Test_Provider_readIvpn(t *testing.T) {
targetIP: singleStringCall{call: true, value: "1.2.3.4"},
countries: sliceStringCall{call: true, values: []string{"A", "B"}},
cities: sliceStringCall{call: true, values: []string{"C", "D"}},
isps: sliceStringCall{call: true, values: []string{"ISP 1"}},
hostnames: sliceStringCall{call: true, values: []string{"E", "F"}},
protocol: singleStringCall{call: true, value: constants.TCP},
settings: Provider{
@@ -108,6 +123,7 @@ func Test_Provider_readIvpn(t *testing.T) {
TargetIP: net.IPv4(1, 2, 3, 4),
Countries: []string{"A", "B"},
Cities: []string{"C", "D"},
ISPs: []string{"ISP 1"},
Hostnames: []string{"E", "F"},
},
},
@@ -136,6 +152,10 @@ func Test_Provider_readIvpn(t *testing.T) {
env.EXPECT().CSVInside("CITY", constants.IvpnCityChoices()).
Return(testCase.cities.values, testCase.cities.err)
}
if testCase.isps.call {
env.EXPECT().CSVInside("ISP", constants.IvpnISPChoices()).
Return(testCase.isps.values, testCase.isps.err)
}
if testCase.hostnames.call {
env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices()).
Return(testCase.hostnames.values, testCase.hostnames.err)

View File

@@ -28,6 +28,15 @@ func IvpnCityChoices() (choices []string) {
return makeUnique(choices)
}
func IvpnISPChoices() (choices []string) {
servers := IvpnServers()
choices = make([]string, len(servers))
for i := range servers {
choices[i] = servers[i].ISP
}
return makeUnique(choices)
}
func IvpnHostnameChoices() (choices []string) {
servers := IvpnServers()
choices = make([]string, len(servers))

View File

@@ -25970,13 +25970,14 @@
]
},
"ivpn": {
"version": 1,
"timestamp": 1629490838,
"version": 2,
"timestamp": 1629589118,
"servers": [
{
"country": "Australia",
"city": "",
"hostname": "au-nsw.gw.ivpn.net",
"city": "Sydney",
"isp": "M247",
"hostname": "au-nsw1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -25985,8 +25986,9 @@
},
{
"country": "Austria",
"city": "",
"hostname": "at.gw.ivpn.net",
"city": "Vienna",
"isp": "M247",
"hostname": "at1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -25995,8 +25997,9 @@
},
{
"country": "Belgium",
"city": "",
"hostname": "be.gw.ivpn.net",
"city": "Brussels",
"isp": "M247",
"hostname": "be1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26005,8 +26008,9 @@
},
{
"country": "Brazil",
"city": "",
"hostname": "br.gw.ivpn.net",
"city": "Franca",
"isp": "Qnax",
"hostname": "br1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26015,8 +26019,9 @@
},
{
"country": "Bulgaria",
"city": "",
"hostname": "bg.gw.ivpn.net",
"city": "Sofia",
"isp": "M247",
"hostname": "bg1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26026,7 +26031,8 @@
{
"country": "Canada",
"city": "Montreal",
"hostname": "ca-qc.gw.ivpn.net",
"isp": "M247",
"hostname": "ca-qc1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26036,17 +26042,30 @@
{
"country": "Canada",
"city": "Toronto",
"hostname": "ca.gw.ivpn.net",
"isp": "Amanah",
"hostname": "ca1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"104.254.90.178"
]
},
{
"country": "Canada",
"city": "Toronto",
"isp": "Amanah",
"hostname": "ca2.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"172.86.186.170"
]
},
{
"country": "Czech Republic",
"city": "",
"hostname": "cz.gw.ivpn.net",
"city": "Prague",
"isp": "Datapacket",
"hostname": "cz1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26055,8 +26074,9 @@
},
{
"country": "Denmark",
"city": "",
"hostname": "dk.gw.ivpn.net",
"city": "Copenhagen",
"isp": "M247",
"hostname": "dk1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26065,8 +26085,9 @@
},
{
"country": "Finland",
"city": "",
"hostname": "fi.gw.ivpn.net",
"city": "Helsinki",
"isp": "Creanova",
"hostname": "fi1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26075,8 +26096,9 @@
},
{
"country": "France",
"city": "",
"hostname": "fr.gw.ivpn.net",
"city": "Paris",
"isp": "Datapacket",
"hostname": "fr1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26085,8 +26107,20 @@
},
{
"country": "Germany",
"city": "",
"hostname": "de.gw.ivpn.net",
"city": "Frankfurt",
"isp": "Leaseweb",
"hostname": "de1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"178.162.222.40"
]
},
{
"country": "Germany",
"city": "Frankfurt",
"isp": "Leaseweb",
"hostname": "de2.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26095,8 +26129,20 @@
},
{
"country": "Hong Kong",
"city": "",
"hostname": "hk.gw.ivpn.net",
"city": "Hong Kong",
"isp": "Leaseweb",
"hostname": "hk1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"209.58.189.163"
]
},
{
"country": "Hong Kong",
"city": "Hong Kong",
"isp": "Leaseweb",
"hostname": "hk2.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26105,8 +26151,9 @@
},
{
"country": "Hungary",
"city": "",
"hostname": "hu.gw.ivpn.net",
"city": "Budapest",
"isp": "M247",
"hostname": "hu1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26115,8 +26162,9 @@
},
{
"country": "Iceland",
"city": "",
"hostname": "is.gw.ivpn.net",
"city": "Reykjavik",
"isp": "Advania",
"hostname": "is1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26125,8 +26173,9 @@
},
{
"country": "Israel",
"city": "",
"hostname": "il.gw.ivpn.net",
"city": "Holon, Tel Aviv",
"isp": "HQServ",
"hostname": "il1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26135,8 +26184,9 @@
},
{
"country": "Italy",
"city": "",
"hostname": "it.gw.ivpn.net",
"city": "Milan",
"isp": "SEFlow",
"hostname": "it1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26145,8 +26195,9 @@
},
{
"country": "Japan",
"city": "",
"hostname": "jp.gw.ivpn.net",
"city": "Tokyo",
"isp": "M247",
"hostname": "jp1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26155,8 +26206,9 @@
},
{
"country": "Luxembourg",
"city": "",
"hostname": "lu.gw.ivpn.net",
"city": "Luxembourg",
"isp": "Evoluso",
"hostname": "lu1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26165,18 +26217,75 @@
},
{
"country": "Netherlands",
"city": "",
"hostname": "nl.gw.ivpn.net",
"city": "Amsterdam",
"isp": "Leaseweb",
"hostname": "nl3.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"95.211.172.68"
]
},
{
"country": "Netherlands",
"city": "Amsterdam",
"isp": "Leaseweb",
"hostname": "nl4.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"95.211.172.95"
]
},
{
"country": "Netherlands",
"city": "Amsterdam",
"isp": "Leaseweb",
"hostname": "nl5.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"95.211.187.222"
]
},
{
"country": "Netherlands",
"city": "Amsterdam",
"isp": "Leaseweb",
"hostname": "nl6.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"95.211.187.228"
]
},
{
"country": "Netherlands",
"city": "Amsterdam",
"isp": "Leaseweb",
"hostname": "nl7.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"95.211.95.22"
]
},
{
"country": "Netherlands",
"city": "Amsterdam",
"isp": "Leaseweb",
"hostname": "nl8.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"95.211.172.18"
]
},
{
"country": "Norway",
"city": "",
"hostname": "no.gw.ivpn.net",
"city": "Oslo",
"isp": "Servethewrld",
"hostname": "no1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26185,8 +26294,9 @@
},
{
"country": "Poland",
"city": "",
"hostname": "pl.gw.ivpn.net",
"city": "Warsaw",
"isp": "Datapacket",
"hostname": "pl1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26195,8 +26305,9 @@
},
{
"country": "Portugal",
"city": "",
"hostname": "pt.gw.ivpn.net",
"city": "Lisbon",
"isp": "Hostwebis",
"hostname": "pt1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26205,8 +26316,9 @@
},
{
"country": "Romania",
"city": "",
"hostname": "ro.gw.ivpn.net",
"city": "Bucharest",
"isp": "M247",
"hostname": "ro1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26215,8 +26327,9 @@
},
{
"country": "Serbia",
"city": "",
"hostname": "rs.gw.ivpn.net",
"city": "Belgrade",
"isp": "M247",
"hostname": "rs1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26225,8 +26338,9 @@
},
{
"country": "Singapore",
"city": "",
"hostname": "sg.gw.ivpn.net",
"city": "Singapore",
"isp": "M247",
"hostname": "sg1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26235,8 +26349,9 @@
},
{
"country": "Slovakia",
"city": "",
"hostname": "sk.gw.ivpn.net",
"city": "Bratislava",
"isp": "M247",
"hostname": "sk1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26245,8 +26360,9 @@
},
{
"country": "Spain",
"city": "",
"hostname": "es.gw.ivpn.net",
"city": "Madrid",
"isp": "Datapacket",
"hostname": "es1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26255,8 +26371,9 @@
},
{
"country": "Sweden",
"city": "",
"hostname": "se.gw.ivpn.net",
"city": "Stockholm",
"isp": "GleSyS",
"hostname": "se1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26265,8 +26382,9 @@
},
{
"country": "Switzerland",
"city": "",
"hostname": "ch.gw.ivpn.net",
"city": "Zurich",
"isp": "M247",
"hostname": "ch1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26274,129 +26392,21 @@
]
},
{
"country": "USA",
"city": "Atlanta",
"hostname": "us-ga.gw.ivpn.net",
"country": "Switzerland",
"city": "Zurich",
"isp": "Privatelayer",
"hostname": "ch3.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"104.129.24.146"
]
},
{
"country": "USA",
"city": "Chicago",
"hostname": "us-il.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"72.11.137.146"
]
},
{
"country": "USA",
"city": "Dallas",
"hostname": "us-tx.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"96.44.189.194"
]
},
{
"country": "USA",
"city": "Las Vegas",
"hostname": "us-nv.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"185.242.5.34"
]
},
{
"country": "USA",
"city": "Los Angeles",
"hostname": "us-ca.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"69.12.80.146"
]
},
{
"country": "USA",
"city": "Miami",
"hostname": "us-fl.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"173.44.49.90"
]
},
{
"country": "USA",
"city": "New Jersey",
"hostname": "us-nj.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"23.226.128.18"
]
},
{
"country": "USA",
"city": "New York",
"hostname": "us-ny.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"64.120.44.114"
]
},
{
"country": "USA",
"city": "Phoenix",
"hostname": "us-az.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"193.37.254.130"
]
},
{
"country": "USA",
"city": "Salt Lake City",
"hostname": "us-ut.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"198.105.216.28"
]
},
{
"country": "USA",
"city": "Seattle",
"hostname": "us-wa.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"23.19.87.209"
]
},
{
"country": "USA",
"city": "Washington",
"hostname": "us-dc.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"207.244.108.207"
"141.255.166.194"
]
},
{
"country": "Ukraine",
"city": "",
"hostname": "ua.gw.ivpn.net",
"city": "Kharkiv",
"isp": "Xservers",
"hostname": "ua1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
@@ -26406,23 +26416,255 @@
{
"country": "United Kingdom",
"city": "London",
"hostname": "gb.gw.ivpn.net",
"isp": "Datapacket",
"hostname": "gb1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"185.59.221.88",
"185.59.221.133"
]
},
{
"country": "United Kingdom",
"city": "London",
"isp": "Datapacket",
"hostname": "gb2.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"185.59.221.88"
]
},
{
"country": "United Kingdom",
"city": "Manchester",
"hostname": "gb-man.gw.ivpn.net",
"isp": "M247",
"hostname": "gb-man1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"89.238.141.228"
]
},
{
"country": "United States",
"city": "Atlanta, GA",
"isp": "Quadranet",
"hostname": "us-ga1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"104.129.24.146"
]
},
{
"country": "United States",
"city": "Atlanta, GA",
"isp": "Quadranet",
"hostname": "us-ga2.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"107.150.22.74"
]
},
{
"country": "United States",
"city": "Chicago, IL",
"isp": "Quadranet",
"hostname": "us-il1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"107.150.28.82"
]
},
{
"country": "United States",
"city": "Chicago, IL",
"isp": "Quadranet",
"hostname": "us-il2.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"72.11.137.146"
]
},
{
"country": "United States",
"city": "Dallas, TX",
"isp": "Quadranet",
"hostname": "us-tx1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"96.44.189.194"
]
},
{
"country": "United States",
"city": "Dallas, TX",
"isp": "Quadranet",
"hostname": "us-tx2.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"96.44.142.74"
]
},
{
"country": "United States",
"city": "Las Vegas, NV",
"isp": "M247",
"hostname": "us-nv1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"185.242.5.34"
]
},
{
"country": "United States",
"city": "Los Angeles, CA",
"isp": "Quadranet",
"hostname": "us-ca1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"173.254.196.58"
]
},
{
"country": "United States",
"city": "Los Angeles, CA",
"isp": "Quadranet",
"hostname": "us-ca2.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"69.12.80.146"
]
},
{
"country": "United States",
"city": "Los Angeles, CA",
"isp": "Leaseweb",
"hostname": "us-ca3.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"209.58.130.196"
]
},
{
"country": "United States",
"city": "Los Angeles, CA",
"isp": "Quadranet",
"hostname": "us-ca4.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"173.254.204.202"
]
},
{
"country": "United States",
"city": "Miami, FL",
"isp": "Quadranet",
"hostname": "us-fl1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"173.44.49.90"
]
},
{
"country": "United States",
"city": "New Jersey, NJ",
"isp": "Quadranet",
"hostname": "us-nj3.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"23.226.128.18"
]
},
{
"country": "United States",
"city": "New Jersey, NJ",
"isp": "M247",
"hostname": "us-nj4.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"194.36.111.50"
]
},
{
"country": "United States",
"city": "New York, NY",
"isp": "Leaseweb",
"hostname": "us-ny1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"64.120.44.114"
]
},
{
"country": "United States",
"city": "New York, NY",
"isp": "Leaseweb",
"hostname": "us-ny2.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"173.234.153.130"
]
},
{
"country": "United States",
"city": "Phoenix, AZ",
"isp": "M247",
"hostname": "us-az1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"193.37.254.130"
]
},
{
"country": "United States",
"city": "Salt Lake City, UT",
"isp": "100TB",
"hostname": "us-ut1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"198.105.216.28"
]
},
{
"country": "United States",
"city": "Seattle, WA",
"isp": "Leaseweb",
"hostname": "us-wa1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"23.19.87.209"
]
},
{
"country": "United States",
"city": "Washington, DC",
"isp": "Leaseweb",
"hostname": "us-dc1.gw.ivpn.net",
"tcp": false,
"udp": true,
"ips": [
"207.244.108.207"
]
}
]
},

View File

@@ -68,7 +68,7 @@ func Test_versions(t *testing.T) {
"Ivpn": {
model: models.IvpnServer{},
version: allServers.Ivpn.Version,
digest: "2eb80d28",
digest: "abdc2848",
},
"Mullvad": {
model: models.MullvadServer{},

View File

@@ -41,6 +41,7 @@ type IpvanishServer struct {
type IvpnServer struct {
Country string `json:"country"`
City string `json:"city"`
ISP string `json:"isp"`
Hostname string `json:"hostname"`
TCP bool `json:"tcp"`
UDP bool `json:"udp"`

View File

@@ -11,6 +11,7 @@ func (i *Ivpn) filterServers(selection configuration.ServerSelection) (
for _, server := range i.servers {
switch {
case
utils.FilterByPossibilities(server.ISP, selection.ISPs),
utils.FilterByPossibilities(server.Country, selection.Countries),
utils.FilterByPossibilities(server.City, selection.Cities),
utils.FilterByPossibilities(server.Hostname, selection.Hostnames),

View File

@@ -110,7 +110,7 @@ func (u *updater) updateIpvanish(ctx context.Context) (err error) {
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)
ctx, u.client, u.presolver, minServers)
if u.options.CLI {
for _, warning := range warnings {
u.logger.Warn("Ivpn: " + warning)

View File

@@ -0,0 +1,66 @@
package ivpn
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
)
var (
errBuildRequest = errors.New("cannot build HTTP request")
errDoRequest = errors.New("failed doing HTTP request")
errHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
errUnmarshalResponseBody = errors.New("failed unmarshaling response body")
errCloseBody = errors.New("failed closing HTTP body")
)
type apiData struct {
Servers []apiServer `json:"servers"`
}
type apiServer struct {
Hostnames apiHostnames `json:"hostnames"`
IsActive bool `json:"is_active"`
Country string `json:"country"`
City string `json:"city"`
ISP string `json:"isp"`
}
type apiHostnames struct {
OpenVPN string `json:"openvpn"`
}
func fetchAPI(ctx context.Context, client *http.Client) (
data apiData, err error) {
const url = "https://api.ivpn.net/v4/servers/stats"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return data, fmt.Errorf("%w: %s", errBuildRequest, err)
}
response, err := client.Do(request)
if err != nil {
return data, fmt.Errorf("%w: %s", errDoRequest, err)
}
if response.StatusCode != http.StatusOK {
_ = response.Body.Close()
return data, fmt.Errorf("%w: %d %s",
errHTTPStatusCodeNotOK, response.StatusCode, response.Status)
}
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&data); err != nil {
_ = response.Body.Close()
return data, fmt.Errorf("%w: %s", errUnmarshalResponseBody, err)
}
if err := response.Body.Close(); err != nil {
return data, fmt.Errorf("%w: %s", errCloseBody, err)
}
return data, nil
}

View File

@@ -0,0 +1,96 @@
package ivpn
import (
"context"
"errors"
"io"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_fetchAPI(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
responseStatus int
responseBody io.ReadCloser
data apiData
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(`{}`)),
},
"success": {
responseStatus: http.StatusOK,
responseBody: ioutil.NopCloser(strings.NewReader(`{"servers":[
{"country":"Country1","city":"City A","isp":"xyz","is_active":true,"hostnames":{"openvpn":"hosta"}},
{"country":"Country2","city":"City B","isp":"abc","is_active":false,"hostnames":{"openvpn":"hostb"}}
]}`)),
data: apiData{
Servers: []apiServer{
{
Country: "Country1",
City: "City A",
IsActive: true,
ISP: "xyz",
Hostnames: apiHostnames{
OpenVPN: "hosta",
},
},
{
Country: "Country2",
City: "City B",
ISP: "abc",
Hostnames: apiHostnames{
OpenVPN: "hostb",
},
},
},
},
},
}
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://api.ivpn.net/v4/servers/stats")
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

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

View File

@@ -1,41 +0,0 @@
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)
})
}
}

View File

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

View File

@@ -1,211 +0,0 @@
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)
})
}
}

View File

@@ -0,0 +1,9 @@
package ivpn
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,78 +6,64 @@ 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 (
ErrFetchAPI = errors.New("failed fetching API")
ErrNotEnoughServers = errors.New("not enough servers found")
)
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
func GetServers(ctx context.Context, client *http.Client,
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)
data, err := fetchAPI(ctx, client)
if err != nil {
return nil, nil, err
} else if len(contents) < minServers {
return nil, nil, fmt.Errorf("%w: %s", ErrFetchAPI, err)
}
hosts := make([]string, 0, len(data.Servers))
for _, serverData := range data.Servers {
host := serverData.Hostnames.OpenVPN
if host == "" {
continue // Wireguard
}
hosts = append(hosts, host)
}
if len(hosts) < minServers {
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
ErrNotEnoughServers, len(contents), minServers)
ErrNotEnoughServers, len(hosts), 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...)
hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers)
if err != nil {
return nil, warnings, err
}
hts.adaptWithIPs(hostToIPs)
servers = make([]models.IvpnServer, 0, len(hosts))
for _, serverData := range data.Servers {
host := serverData.Hostnames.OpenVPN
if serverData.Hostnames.OpenVPN == "" {
continue // Wireguard
}
servers = hts.toServersSlice()
if len(servers) < minServers {
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
ErrNotEnoughServers, len(servers), minServers)
server := models.IvpnServer{
Country: serverData.Country,
City: serverData.City,
ISP: serverData.ISP,
Hostname: serverData.Hostnames.OpenVPN,
// TCP is not supported
UDP: true,
IPs: hostToIPs[host],
}
servers = append(servers, server)
}
sortServers(servers)

View File

@@ -3,27 +3,30 @@ package ivpn
import (
"context"
"errors"
"io/ioutil"
"net"
"net/http"
"strings"
"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
// From API
responseBody string
responseStatus int
// Resolution
expectResolve bool
@@ -38,47 +41,15 @@ func Test_GetServers(t *testing.T) {
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"),
"http response error": {
responseStatus: http.StatusNoContent,
err: errors.New("failed fetching API: HTTP status code not OK: 204 No Content"),
},
"resolve error": {
unzipContents: map[string][]byte{
"config.ovpn": []byte("remote hosta"),
},
responseBody: `{"servers":[
{"hostnames":{"openvpn":"hosta"}}
]}`,
responseStatus: http.StatusOK,
expectResolve: true,
hostsToResolve: []string{"hosta"},
resolveSettings: getResolveSettings(0),
@@ -87,12 +58,22 @@ func Test_GetServers(t *testing.T) {
warnings: []string{"resolve warning"},
err: errors.New("dummy"),
},
"not enough servers": {
minServers: 2,
responseBody: `{"servers":[
{"hostnames":{"openvpn":"hosta"}}
]}`,
responseStatus: http.StatusOK,
err: errors.New("not enough servers found: 1 and expected at least 2"),
},
"success": {
minServers: 1,
unzipContents: map[string][]byte{
"Country1-City_A.ovpn": []byte("remote hosta"),
"Country2-City_B.ovpn": []byte("remote hostb"),
},
responseBody: `{"servers":[
{"country":"Country1","city":"City A","hostnames":{"openvpn":"hosta"}},
{"country":"Country2","city":"City B","hostnames":{"openvpn":"hostb"}},
{"country":"Country3","city":"City C","hostnames":{"wireguard":"hostc"}}
]}`,
responseStatus: http.StatusOK,
expectResolve: true,
hostsToResolve: []string{"hosta", "hostb"},
resolveSettings: getResolveSettings(1),
@@ -116,10 +97,17 @@ func Test_GetServers(t *testing.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)
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://api.ivpn.net/v4/servers/stats")
return &http.Response{
StatusCode: testCase.responseStatus,
Status: http.StatusText(testCase.responseStatus),
Body: ioutil.NopCloser(strings.NewReader(testCase.responseBody)),
}, nil
}),
}
presolver := mock_resolver.NewMockParallel(ctrl)
if testCase.expectResolve {
@@ -127,7 +115,7 @@ func Test_GetServers(t *testing.T) {
Return(testCase.hostToIPs, testCase.resolveWarnings, testCase.resolveErr)
}
servers, warnings, err := GetServers(ctx, unzipper, presolver, testCase.minServers)
servers, warnings, err := GetServers(ctx, client, presolver, testCase.minServers)
assert.Equal(t, testCase.servers, servers)
assert.Equal(t, testCase.warnings, warnings)