package updater import ( "encoding/base64" "errors" "fmt" "net/netip" "strings" ) // Check out the JSON data from https://api.nordvpn.com/v2/servers?limit=10 type serversData struct { Servers []serverData `json:"servers"` Groups []groupData `json:"groups"` Services []serviceData `json:"services"` Locations []locationData `json:"locations"` Technologies []technologyData `json:"technologies"` } type serverData struct { // Name is the server name, for example 'Poland #128' Name string `json:"name"` // Stations is, it seems, the entry IP address. // However it is ignored in favor of the 'ips' entry field. Station netip.Addr `json:"station"` // IPv6Station is mostly empty, so we ignore it for now. IPv6Station netip.Addr `json:"station_ipv6"` // Hostname is the server hostname, for example 'pl128.nordvpn.com' Hostname string // Status is the server status, for example 'online' Status string `json:"status"` // Locations is the list of location IDs for the server. // Only the first location is taken into account for now. LocationIDs []uint32 `json:"location_ids"` Technologies []struct { ID uint32 `json:"id"` Status string `json:"status"` Metadata []struct { // Name can notably be 'public_key'. Name string `json:"name"` // Value can notably the Wireguard public key value. Value string `json:"value"` } `json:"metadata"` } `json:"technologies"` GroupIDs []uint32 `json:"group_ids"` ServiceIDs []uint32 `json:"service_ids"` // IPs is the list of IP addresses for the server. IPs []struct { // Type can notably be 'entry'. Type string `json:"type"` IP struct { IP netip.Addr `json:"ip"` } `json:"ip"` } `json:"ips"` } type groupData struct { ID uint32 `json:"id"` Title string `json:"title"` // "Europe", "Standard VPN servers", etc. Type struct { Identifier string `json:"identifier"` // 'regions', 'legacy_group_category', etc. } `json:"type"` } type serviceData struct { ID uint32 `json:"id"` Identifier string `json:"identifier"` // 'vpn', 'proxy', etc. } type locationData struct { ID uint32 `json:"id"` Country struct { Name string `json:"name"` // for example "Poland" City struct { Name string `json:"name"` // for example "Warsaw" } `json:"city"` } `json:"country"` } type technologyData struct { ID uint32 `json:"id"` // Identifier is the technology identifier name and relevant values are: // 'openvpn_udp', 'openvpn_tcp', 'openvpn_dedicated_udp', // 'openvpn_dedicated_tcp' and 'wireguard_udp' Identifier string `json:"identifier"` } func (s serversData) idToData() ( groups map[uint32]groupData, services map[uint32]serviceData, locations map[uint32]locationData, technologies map[uint32]technologyData, ) { groups = make(map[uint32]groupData, len(s.Groups)) for _, group := range s.Groups { if group.Type.Identifier == "regions" { //nolint:goconst group.Title = strings.ReplaceAll(group.Title, ",", "") } groups[group.ID] = group } services = make(map[uint32]serviceData, len(s.Services)) for _, service := range s.Services { services[service.ID] = service } locations = make(map[uint32]locationData, len(s.Locations)) for _, location := range s.Locations { locations[location.ID] = location } technologies = make(map[uint32]technologyData, len(s.Technologies)) for _, technology := range s.Technologies { technologies[technology.ID] = technology } return groups, services, locations, technologies } func (s *serverData) region(groups map[uint32]groupData) (region string) { for _, groupID := range s.GroupIDs { group, ok := groups[groupID] if !ok { continue } if group.Type.Identifier == "regions" { return group.Title } } return "" } func (s *serverData) hasVPNService(services map[uint32]serviceData) (ok bool) { for _, serviceID := range s.ServiceIDs { service, ok := services[serviceID] if !ok { continue } if service.Identifier == "vpn" { return true } } 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)) for _, ipObject := range s.IPs { if ipObject.Type != "entry" { continue } ips = append(ips, ipObject.IP.IP) } return ips } var ( ErrWireguardPublicKeyMalformed = errors.New("wireguard public key is malformed") ErrWireguardPublicKeyNotFound = errors.New("wireguard public key not found") ) // wireguardPublicKey returns the Wireguard public key for the server. func (s *serverData) wireguardPublicKey(technologies map[uint32]technologyData) ( wgPubKey string, err error) { for _, technology := range s.Technologies { data, ok := technologies[technology.ID] if !ok || data.Identifier != "wireguard_udp" { continue } for _, metadata := range technology.Metadata { if metadata.Name != "public_key" { continue } wgPubKey = metadata.Value _, err = base64.StdEncoding.DecodeString(wgPubKey) if err != nil { return "", fmt.Errorf("%w: %s cannot be decoded: %s", ErrWireguardPublicKeyMalformed, wgPubKey, err) } return metadata.Value, nil } } return "", fmt.Errorf("%w", ErrWireguardPublicKeyNotFound) }