diff --git a/internal/configuration/ivpn.go b/internal/configuration/ivpn.go index 992bc319..0b626ecd 100644 --- a/internal/configuration/ivpn.go +++ b/internal/configuration/ivpn.go @@ -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) diff --git a/internal/configuration/ivpn_test.go b/internal/configuration/ivpn_test.go index 75e8b5fa..85357a5c 100644 --- a/internal/configuration/ivpn_test.go +++ b/internal/configuration/ivpn_test.go @@ -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) diff --git a/internal/constants/ivpn.go b/internal/constants/ivpn.go index 82f868cd..98417dad 100644 --- a/internal/constants/ivpn.go +++ b/internal/constants/ivpn.go @@ -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)) diff --git a/internal/constants/servers.json b/internal/constants/servers.json index 35f94593..f90b8bb4 100644 --- a/internal/constants/servers.json +++ b/internal/constants/servers.json @@ -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" + ] } ] }, diff --git a/internal/constants/servers_test.go b/internal/constants/servers_test.go index 5e1541bc..c58a6dfc 100644 --- a/internal/constants/servers_test.go +++ b/internal/constants/servers_test.go @@ -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{}, diff --git a/internal/models/server.go b/internal/models/server.go index 08859802..9b1dc673 100644 --- a/internal/models/server.go +++ b/internal/models/server.go @@ -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"` diff --git a/internal/provider/ivpn/filter.go b/internal/provider/ivpn/filter.go index 385680d7..74e1617b 100644 --- a/internal/provider/ivpn/filter.go +++ b/internal/provider/ivpn/filter.go @@ -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), diff --git a/internal/updater/providers.go b/internal/updater/providers.go index d264e464..7e80e567 100644 --- a/internal/updater/providers.go +++ b/internal/updater/providers.go @@ -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) diff --git a/internal/updater/providers/ivpn/api.go b/internal/updater/providers/ivpn/api.go new file mode 100644 index 00000000..3f02820f --- /dev/null +++ b/internal/updater/providers/ivpn/api.go @@ -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 +} diff --git a/internal/updater/providers/ivpn/api_test.go b/internal/updater/providers/ivpn/api_test.go new file mode 100644 index 00000000..8e99cbf1 --- /dev/null +++ b/internal/updater/providers/ivpn/api_test.go @@ -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) + } + }) + } +} diff --git a/internal/updater/providers/ivpn/filename.go b/internal/updater/providers/ivpn/filename.go deleted file mode 100644 index f820e3c6..00000000 --- a/internal/updater/providers/ivpn/filename.go +++ /dev/null @@ -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 -} diff --git a/internal/updater/providers/ivpn/filename_test.go b/internal/updater/providers/ivpn/filename_test.go deleted file mode 100644 index a9729c0e..00000000 --- a/internal/updater/providers/ivpn/filename_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/internal/updater/providers/ivpn/hosttoserver.go b/internal/updater/providers/ivpn/hosttoserver.go deleted file mode 100644 index cc7869cc..00000000 --- a/internal/updater/providers/ivpn/hosttoserver.go +++ /dev/null @@ -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 -} diff --git a/internal/updater/providers/ivpn/hosttoserver_test.go b/internal/updater/providers/ivpn/hosttoserver_test.go deleted file mode 100644 index 7b3c0e9e..00000000 --- a/internal/updater/providers/ivpn/hosttoserver_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/internal/updater/providers/ivpn/roundtrip_test.go b/internal/updater/providers/ivpn/roundtrip_test.go new file mode 100644 index 00000000..f8ee5d95 --- /dev/null +++ b/internal/updater/providers/ivpn/roundtrip_test.go @@ -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) +} diff --git a/internal/updater/providers/ivpn/servers.go b/internal/updater/providers/ivpn/servers.go index 41a34659..d219c464 100644 --- a/internal/updater/providers/ivpn/servers.go +++ b/internal/updater/providers/ivpn/servers.go @@ -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) diff --git a/internal/updater/providers/ivpn/servers_test.go b/internal/updater/providers/ivpn/servers_test.go index 7cd54701..9f7c8b52 100644 --- a/internal/updater/providers/ivpn/servers_test.go +++ b/internal/updater/providers/ivpn/servers_test.go @@ -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)