Skip to content

Commit

Permalink
feat(nordvpn): filter with SERVER_CATEGORIES (#1806)
Browse files Browse the repository at this point in the history
- update NordVPN servers data built-in
  • Loading branch information
AdamHebby authored Mar 22, 2024
1 parent c74e417 commit b3ceece
Show file tree
Hide file tree
Showing 17 changed files with 111,179 additions and 44,795 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ issues:
- goerr113
- containedctx
- goconst
- maintidx
- path: "internal\\/server\\/.+\\.go"
linters:
- dupl
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
SERVER_COUNTRIES= \
SERVER_CITIES= \
SERVER_HOSTNAMES= \
SERVER_CATEGORIES= \
# # Mullvad only:
ISP= \
OWNED_ONLY=no \
Expand Down
1 change: 1 addition & 0 deletions internal/configuration/settings/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "errors"
var (
ErrCityNotValid = errors.New("the city specified is not valid")
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
ErrCategoryNotValid = errors.New("the category specified is not valid")
ErrCountryNotValid = errors.New("the country specified is not valid")
ErrFilepathMissing = errors.New("filepath is missing")
ErrFirewallZeroPort = errors.New("cannot have a zero port")
Expand Down
16 changes: 15 additions & 1 deletion internal/configuration/settings/serverselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ type ServerSelection struct { //nolint:maligned
// state, and can be set to the unspecified address to indicate
// there is not target IP address to use.
TargetIP netip.Addr `json:"target_ip"`
// Counties is the list of countries to filter VPN servers with.
// Countries is the list of countries to filter VPN servers with.
Countries []string `json:"countries"`
// Categories is the list of categories to filter VPN servers with.
Categories []string `json:"categories"`
// Regions is the list of regions to filter VPN servers with.
Regions []string `json:"regions"`
// Cities is the list of cities to filter VPN servers with.
Expand Down Expand Up @@ -224,6 +226,11 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
}

err = validate.AreAllOneOfCaseInsensitive(settings.Categories, filterChoices.Categories)
if err != nil {
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
}

return nil
}

Expand All @@ -232,6 +239,7 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
VPN: ss.VPN,
TargetIP: ss.TargetIP,
Countries: gosettings.CopySlice(ss.Countries),
Categories: gosettings.CopySlice(ss.Categories),
Regions: gosettings.CopySlice(ss.Regions),
Cities: gosettings.CopySlice(ss.Cities),
ISPs: gosettings.CopySlice(ss.ISPs),
Expand All @@ -253,6 +261,7 @@ func (ss *ServerSelection) mergeWith(other ServerSelection) {
ss.VPN = gosettings.MergeWithString(ss.VPN, other.VPN)
ss.TargetIP = gosettings.MergeWithValidator(ss.TargetIP, other.TargetIP)
ss.Countries = gosettings.MergeWithSlice(ss.Countries, other.Countries)
ss.Categories = gosettings.MergeWithSlice(ss.Categories, other.Categories)
ss.Regions = gosettings.MergeWithSlice(ss.Regions, other.Regions)
ss.Cities = gosettings.MergeWithSlice(ss.Cities, other.Cities)
ss.ISPs = gosettings.MergeWithSlice(ss.ISPs, other.ISPs)
Expand All @@ -274,6 +283,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.VPN = gosettings.OverrideWithString(ss.VPN, other.VPN)
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
ss.Cities = gosettings.OverrideWithSlice(ss.Cities, other.Cities)
ss.ISPs = gosettings.OverrideWithSlice(ss.ISPs, other.ISPs)
Expand Down Expand Up @@ -318,6 +328,10 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
}

if len(ss.Categories) > 0 {
node.Appendf("Categories: %s", strings.Join(ss.Categories, ", "))
}

if len(ss.Regions) > 0 {
node.Appendf("Regions: %s", strings.Join(ss.Regions, ", "))
}
Expand Down
22 changes: 22 additions & 0 deletions internal/configuration/settings/validation/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ func ExtractCountries(servers []models.Server) (values []string) {
return values
}

func ExtractCategories(servers []models.Server) (values []string) {
seen := make(map[string]struct{}, len(servers))
values = make([]string, 0, len(servers))
for _, server := range servers {
categories := server.Categories
if len(categories) == 0 {
continue
}

for _, value := range categories {
_, alreadySeen := seen[value]
if alreadySeen {
continue
}
seen[value] = struct{}{}

values = sortedInsert(values, value)
}
}
return values
}

func ExtractRegions(servers []models.Server) (values []string) {
seen := make(map[string]struct{}, len(servers))
values = make([]string, 0, len(servers))
Expand Down
1 change: 1 addition & 0 deletions internal/configuration/sources/env/serverselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func (s *Source) readServerSelection(vpnProvider, vpnType string) (
ss.Hostnames = s.env.CSV("SERVER_HOSTNAMES", env.RetroKeys("SERVER_HOSTNAME"))
ss.Names = s.env.CSV("SERVER_NAMES", env.RetroKeys("SERVER_NAME"))
ss.Numbers, err = s.env.CSVUint16("SERVER_NUMBER")
ss.Categories = s.env.CSV("SERVER_CATEGORIES")
if err != nil {
return ss, err
}
Expand Down
13 changes: 7 additions & 6 deletions internal/models/filters.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package models

type FilterChoices struct {
Countries []string
Regions []string
Cities []string
ISPs []string
Names []string
Hostnames []string
Countries []string
Regions []string
Cities []string
Categories []string
ISPs []string
Names []string
Hostnames []string
}
5 changes: 4 additions & 1 deletion internal/models/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func markdownTableHeading(legendFields ...string) (markdown string) {
const (
cityHeader = "City"
countryHeader = "Country"
categoriesHeader = "Categories"
freeHeader = "Free"
hostnameHeader = "Hostname"
ispHeader = "ISP"
Expand Down Expand Up @@ -51,6 +52,8 @@ func (s *Server) ToMarkdown(headers ...string) (markdown string) {
fields[i] = s.City
case countryHeader:
fields[i] = s.Country
case categoriesHeader:
fields[i] = strings.Join(s.Categories, ", ")
case freeHeader:
fields[i] = boolToMarkdown(s.Free)
case hostnameHeader:
Expand Down Expand Up @@ -120,7 +123,7 @@ func getMarkdownHeaders(vpnProvider string) (headers []string) {
case providers.Mullvad:
return []string{countryHeader, cityHeader, ispHeader, ownedHeader, hostnameHeader, vpnHeader}
case providers.Nordvpn:
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader}
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, categoriesHeader}
case providers.Perfectprivacy:
return []string{cityHeader, tcpHeader, udpHeader}
case providers.Privado:
Expand Down
1 change: 1 addition & 0 deletions internal/models/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Server struct {
Region string `json:"region,omitempty"`
City string `json:"city,omitempty"`
ISP string `json:"isp,omitempty"`
Categories []string `json:"categories,omitempty"`
Owned bool `json:"owned,omitempty"`
Number uint16 `json:"number,omitempty"`
ServerName string `json:"server_name,omitempty"`
Expand Down
13 changes: 13 additions & 0 deletions internal/provider/nordvpn/updater/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ func (s *serverData) hasVPNService(services map[uint32]serviceData) (ok bool) {
return false
}

// categories returns the list of categories for the server.
func (s *serverData) categories(groups map[uint32]groupData) (categories []string) {
categories = make([]string, 0, len(s.GroupIDs))
for _, groupID := range s.GroupIDs {
data, ok := groups[groupID]
if !ok || data.Type.Identifier == "regions" {
continue
}
categories = append(categories, data.Title)
}
return categories
}

// ips returns the list of IP addresses for the server.
func (s *serverData) ips() (ips []netip.Addr) {
ips = make([]netip.Addr, 0, len(s.IPs))
Expand Down
11 changes: 6 additions & 5 deletions internal/provider/nordvpn/updater/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,12 @@ func extractServers(jsonServer serverData, groups map[uint32]groupData,
}

server := models.Server{
Country: location.Country.Name,
Region: jsonServer.region(groups),
City: location.Country.City.Name,
Hostname: jsonServer.Hostname,
IPs: jsonServer.ips(),
Country: location.Country.Name,
Region: jsonServer.region(groups),
City: location.Country.City.Name,
Categories: jsonServer.categories(groups),
Hostname: jsonServer.Hostname,
IPs: jsonServer.ips(),
}

number, err := parseServerName(jsonServer.Name)
Expand Down
18 changes: 18 additions & 0 deletions internal/provider/utils/filtering.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ func filterServer(server models.Server,
return true
}

if filterAnyByPossibilities(server.Categories, selection.Categories) {
return true
}

if filterByPossibilities(server.Region, selection.Regions) {
return true
}
Expand Down Expand Up @@ -101,3 +105,17 @@ func filterByPossibilities[T string | uint16](value T, possibilities []T) (filte
}
return true
}

func filterAnyByPossibilities(values, possibilities []string) (filtered bool) {
if len(possibilities) == 0 {
return false
}

for _, value := range values {
if !filterByPossibilities(value, possibilities) {
return false // found a valid value
}
}

return true
}
13 changes: 13 additions & 0 deletions internal/provider/utils/filtering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,19 @@ func Test_FilterServers(t *testing.T) {
{City: "b", VPN: vpn.OpenVPN, UDP: true},
},
},
"filter by category": {
selection: settings.ServerSelection{
Categories: []string{"legacy_p2p"},
}.WithDefaults(providers.Nordvpn),
servers: []models.Server{
{Categories: []string{"legacy_p2p"}, VPN: vpn.OpenVPN, UDP: true},
{Categories: []string{"legacy_standard"}, VPN: vpn.OpenVPN, UDP: true},
{VPN: vpn.OpenVPN, UDP: true},
},
filtered: []models.Server{
{Categories: []string{"legacy_p2p"}, VPN: vpn.OpenVPN, UDP: true},
},
},
"filter by ISP": {
selection: settings.ServerSelection{
ISPs: []string{"b"},
Expand Down
13 changes: 7 additions & 6 deletions internal/storage/choices.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ func (s *Storage) GetFilterChoices(provider string) models.FilterChoices {
serversObject := s.getMergedServersObject(provider)
servers := serversObject.Servers
return models.FilterChoices{
Countries: validation.ExtractCountries(servers),
Regions: validation.ExtractRegions(servers),
Cities: validation.ExtractCities(servers),
ISPs: validation.ExtractISPs(servers),
Names: validation.ExtractServerNames(servers),
Hostnames: validation.ExtractHostnames(servers),
Countries: validation.ExtractCountries(servers),
Categories: validation.ExtractCategories(servers),
Regions: validation.ExtractRegions(servers),
Cities: validation.ExtractCities(servers),
ISPs: validation.ExtractISPs(servers),
Names: validation.ExtractServerNames(servers),
Hostnames: validation.ExtractHostnames(servers),
}
}
18 changes: 18 additions & 0 deletions internal/storage/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ func filterServer(server models.Server,
return true
}

if filterAnyByPossibilities(server.Categories, selection.Categories) {
return true
}

if filterByPossibilities(server.Region, selection.Regions) {
return true
}
Expand Down Expand Up @@ -123,6 +127,20 @@ func filterByPossibilities[T string | uint16](value T, possibilities []T) (filte
return true
}

func filterAnyByPossibilities(values, possibilities []string) (filtered bool) {
if len(possibilities) == 0 {
return false
}

for _, value := range values {
if !filterByPossibilities(value, possibilities) {
return false // found a valid value
}
}

return true
}

func filterByProtocol(selection settings.ServerSelection,
serverTCP, serverUDP bool) (filtered bool) {
switch selection.VPN {
Expand Down
10 changes: 10 additions & 0 deletions internal/storage/formatting.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ func noServerFoundError(selection settings.ServerSelection) (err error) {
messageParts = append(messageParts, part)
}

switch len(selection.Categories) {
case 0:
case 1:
part := "category " + selection.Categories[0]
messageParts = append(messageParts, part)
default:
part := "categories " + commaJoin(selection.Categories)
messageParts = append(messageParts, part)
}

switch len(selection.Regions) {
case 0:
case 1:
Expand Down
Loading

0 comments on commit b3ceece

Please sign in to comment.