feat: SlickVPN Support (#961)
- `internal/updater/html` package - Add unit tests for slickvpn updating code - Change shared html package to be more share-able - Split html utilities in multiple files - Fix processing .ovpn files with prefix space Authored by @Rohaq Co-authored-by: Quentin McGaw <quentin.mcgaw@gmail.com>
This commit is contained in:
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -54,6 +54,7 @@ body:
|
||||
- PrivateVPN
|
||||
- ProtonVPN
|
||||
- PureVPN
|
||||
- SlickVPN
|
||||
- Surfshark
|
||||
- TorGuard
|
||||
- VPNUnlimited
|
||||
|
||||
3
.github/labels.yml
vendored
3
.github/labels.yml
vendored
@@ -64,6 +64,9 @@
|
||||
- name: ":cloud: PureVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: SlickVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: Surfshark"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
|
||||
2
go.mod
2
go.mod
@@ -17,6 +17,7 @@ require (
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
||||
golang.org/x/text v0.3.7
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
|
||||
@@ -40,6 +41,5 @@ require (
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
3
go.sum
3
go.sum
@@ -179,8 +179,9 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
|
||||
@@ -82,6 +82,9 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
case providers.Protonvpn:
|
||||
allowedTCP = []uint16{443, 5995, 8443}
|
||||
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
|
||||
case providers.SlickVPN:
|
||||
allowedTCP = []uint16{443, 8080, 8888}
|
||||
allowedUDP = []uint16{443, 8080, 8888}
|
||||
case providers.Wevpn:
|
||||
allowedTCP = []uint16{53, 1195, 1199, 2018}
|
||||
allowedUDP = []uint16{80, 1194, 1198}
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
Privatevpn = "privatevpn"
|
||||
Protonvpn = "protonvpn"
|
||||
Purevpn = "purevpn"
|
||||
SlickVPN = "slickvpn"
|
||||
Surfshark = "surfshark"
|
||||
Torguard = "torguard"
|
||||
VPNUnlimited = "vpn unlimited"
|
||||
@@ -44,6 +45,7 @@ func All() []string {
|
||||
Privatevpn,
|
||||
Protonvpn,
|
||||
Purevpn,
|
||||
SlickVPN,
|
||||
Surfshark,
|
||||
Torguard,
|
||||
VPNUnlimited,
|
||||
|
||||
@@ -123,6 +123,8 @@ func getMarkdownHeaders(vpnProvider string) (headers []string) {
|
||||
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, freeHeader}
|
||||
case providers.Purevpn:
|
||||
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}
|
||||
case providers.SlickVPN:
|
||||
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader}
|
||||
case providers.Surfshark:
|
||||
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader, multiHopHeader, tcpHeader, udpHeader}
|
||||
case providers.Torguard:
|
||||
|
||||
@@ -33,5 +33,11 @@ func getHostToURL(ctx context.Context, client *http.Client, protocol string) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return openvpn.FetchMultiFiles(ctx, client, urls)
|
||||
const failEarly = true
|
||||
hostToURL, errors := openvpn.FetchMultiFiles(ctx, client, urls, failEarly)
|
||||
if len(errors) > 0 {
|
||||
return nil, errors[0]
|
||||
}
|
||||
|
||||
return hostToURL, nil
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/provider/privatevpn"
|
||||
"github.com/qdm12/gluetun/internal/provider/protonvpn"
|
||||
"github.com/qdm12/gluetun/internal/provider/purevpn"
|
||||
"github.com/qdm12/gluetun/internal/provider/slickvpn"
|
||||
"github.com/qdm12/gluetun/internal/provider/surfshark"
|
||||
"github.com/qdm12/gluetun/internal/provider/torguard"
|
||||
"github.com/qdm12/gluetun/internal/provider/vpnunlimited"
|
||||
@@ -71,6 +72,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
||||
providers.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||
providers.Purevpn: purevpn.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
|
||||
providers.Torguard: torguard.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||
providers.VPNUnlimited: vpnunlimited.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||
|
||||
13
internal/provider/slickvpn/connection.go
Normal file
13
internal/provider/slickvpn/connection.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package slickvpn
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
func (p *Provider) GetConnection(selection settings.ServerSelection) (
|
||||
connection models.Connection, err error) {
|
||||
defaults := utils.NewConnectionDefaults(0, 443, 0) //nolint:gomnd
|
||||
return utils.GetConnection(p.Name(), p.storage, selection, defaults, p.randSource)
|
||||
}
|
||||
30
internal/provider/slickvpn/openvpnconf.go
Normal file
30
internal/provider/slickvpn/openvpnconf.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package slickvpn
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
settings settings.OpenVPN) (lines []string) {
|
||||
const pingSeconds = 10
|
||||
const bufSize = 393216
|
||||
providerSettings := utils.OpenVPNProviderSettings{
|
||||
RemoteCertTLS: true,
|
||||
AuthUserPass: true,
|
||||
Ciphers: []string{
|
||||
openvpn.AES256cbc,
|
||||
},
|
||||
Ping: pingSeconds,
|
||||
SndBuf: bufSize,
|
||||
RcvBuf: bufSize,
|
||||
// Certificate found from https://www.slickvpn.com/tutorials/using-openvpn-configuration-files/
|
||||
CA: "MIIESDCCAzCgAwIBAgIJAKHK5bbBPSU2MA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNVBAYTAlVTMQwwCgYDVQQIEwNWUE4xDDAKBgNVBAcTA1ZQTjEMMAoGA1UEChMDVlBOMQwwCgYDVQQLEwNWUE4xDDAKBgNVBAMTA1ZQTjEMMAoGA1UEKRMDVlBOMRIwEAYJKoZIhvcNAQkBFgNWUE4wHhcNMjIwMjE0MjEzNDQwWhcNMzIwMjEyMjEzNDQwWjB1MQswCQYDVQQGEwJVUzEMMAoGA1UECBMDVlBOMQwwCgYDVQQHEwNWUE4xDDAKBgNVBAoTA1ZQTjEMMAoGA1UECxMDVlBOMQwwCgYDVQQDEwNWUE4xDDAKBgNVBCkTA1ZQTjESMBAGCSqGSIb3DQEJARYDVlBOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUl1XkfGo3c1uFsvgbO3C3yvu0+cHs9IUSsju5U9cPNCo53mqRHU/qntCC+ldIDKN+dNWn7eURIDszy+flutkgucs0qgETy5fzpXnVMtiKmMiOYWiJDor7j7QivRaxoT/O2fyjxvVCL8gMa60ekWSGBT6isYY8t8BnwTPVP0KvDm36wdaqLmubhf2XGvka/hhNx0SXMmz2x3OJ8BcoypZVLLk/+Qm6DJh1KxyDi4kI+jBC41QuaKKDNwb0kth1304eqZoUeCXtGkzl91y76ODAfdqzXf9WYJdgkXpOm53Cg7FtB42AqPRqHJVwYxDnQyrFwy4a3CWqFJnKtxJM/WlwIDAQABo4HaMIHXMB0GA1UdDgQWBBRSzxAtISfbSRPr0fmhwNZc8kOeKzCBpwYDVR0jBIGfMIGcgBRSzxAtISfbSRPr0fmhwNZc8kOeK6F5pHcwdTELMAkGA1UEBhMCVVMxDDAKBgNVBAgTA1ZQTjEMMAoGA1UEBxMDVlBOMQwwCgYDVQQKEwNWUE4xDDAKBgNVBAsTA1ZQTjEMMAoGA1UEAxMDVlBOMQwwCgYDVQQpEwNWUE4xEjAQBgkqhkiG9w0BCQEWA1ZQToIJAKHK5bbBPSU2MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAGuKFW765F3D5wax5IFSQbEtr+rVHgjR8jiYTzxOCmbLaU4oj6phOhfQJiTTADQYgCIC/DN0HsAEEqrKkwEn8KdAoNiAWfqCV/eqnK83y7yRDGx6/zfsch+PAzKZouMJLrvR9RYbHq7m3adZv84YLge7FE1JqFk1j6rSa4dUUnGQPrQgr9Sasnz8O8KK45XH6fqKrsd4p485n+BXPDzWVsHl4M5dqQV7qUZTazbzzh4NyP5/RQ6Oh5jqMN7po4qiqWv1pu0EKDxUG6gcECc2cTQwHhIOPeCGdHS7uuI2FlLnHaCUFBgi8zTsZxaeaPuPch5M7Zxbdg0GBhS2SmNi+io=", //nolint:lll
|
||||
ExtraLines: []string{
|
||||
"redirect-gateway",
|
||||
},
|
||||
}
|
||||
return utils.OpenVPNConfig(providerSettings, connection, settings)
|
||||
}
|
||||
33
internal/provider/slickvpn/provider.go
Normal file
33
internal/provider/slickvpn/provider.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package slickvpn
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/provider/slickvpn/updater"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
storage common.Storage
|
||||
randSource rand.Source
|
||||
utils.NoPortForwarder
|
||||
common.Fetcher
|
||||
}
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
client *http.Client, updaterWarner common.Warner,
|
||||
parallelResolver common.ParallelResolver) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
NoPortForwarder: utils.NewNoPortForwarding(providers.SlickVPN),
|
||||
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() string {
|
||||
return providers.SlickVPN
|
||||
}
|
||||
26
internal/provider/slickvpn/updater/helpers_test.go
Normal file
26
internal/provider/slickvpn/updater/helpers_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func parseTestHTML(t *testing.T, htmlString string) *html.Node {
|
||||
t.Helper()
|
||||
rootNode, err := html.Parse(strings.NewReader(htmlString))
|
||||
require.NoError(t, err)
|
||||
return rootNode
|
||||
}
|
||||
|
||||
func parseTestDataIndexHTML(t *testing.T) *html.Node {
|
||||
t.Helper()
|
||||
|
||||
data, err := os.ReadFile("testdata/index.html")
|
||||
require.NoError(t, err)
|
||||
|
||||
return parseTestHTML(t, string(data))
|
||||
}
|
||||
28
internal/provider/slickvpn/updater/resolve.go
Normal file
28
internal/provider/slickvpn/updater/resolve.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 20 * time.Second
|
||||
betweenDuration = time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
return resolver.ParallelSettings{
|
||||
Hosts: hosts,
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
BetweenDuration: betweenDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
82
internal/provider/slickvpn/updater/servers.go
Normal file
82
internal/provider/slickvpn/updater/servers.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
)
|
||||
|
||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||
servers []models.Server, err error) {
|
||||
hostToData, err := fetchServers(ctx, u.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching and parsing website: %w", err)
|
||||
}
|
||||
|
||||
openvpnURLs := make([]string, 0, len(hostToData))
|
||||
for _, data := range hostToData {
|
||||
openvpnURLs = append(openvpnURLs, data.ovpnURL)
|
||||
}
|
||||
|
||||
if len(openvpnURLs) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(openvpnURLs), minServers)
|
||||
}
|
||||
|
||||
const failEarly = false // some URLs from the website are not valid
|
||||
udpHostToURL, errors := openvpn.FetchMultiFiles(ctx, u.client, openvpnURLs, failEarly)
|
||||
for _, err := range errors {
|
||||
u.warner.Warn(fmt.Sprintf("fetching OpenVPN files: %s", err))
|
||||
}
|
||||
|
||||
if len(udpHostToURL) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(udpHostToURL), minServers)
|
||||
}
|
||||
|
||||
hosts := make([]string, 0, len(udpHostToURL))
|
||||
for host := range udpHostToURL {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
resolveSettings := parallelResolverSettings(hosts)
|
||||
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||
for _, warning := range warnings {
|
||||
u.warner.Warn(warning)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving hosts: %w", err)
|
||||
}
|
||||
|
||||
if len(hostToIPs) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(hosts), minServers)
|
||||
}
|
||||
|
||||
servers = make([]models.Server, 0, len(hostToIPs))
|
||||
for host, IPs := range hostToIPs {
|
||||
_, udp := udpHostToURL[host]
|
||||
|
||||
serverData := hostToData[host]
|
||||
|
||||
server := models.Server{
|
||||
VPN: vpn.OpenVPN,
|
||||
Region: serverData.region,
|
||||
Country: serverData.country,
|
||||
City: serverData.city,
|
||||
Hostname: host,
|
||||
UDP: udp,
|
||||
IPs: IPs,
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
sort.Sort(models.SortableServers(servers))
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
236
internal/provider/slickvpn/updater/testdata/index.html
vendored
Normal file
236
internal/provider/slickvpn/updater/testdata/index.html
vendored
Normal file
File diff suppressed because one or more lines are too long
22
internal/provider/slickvpn/updater/updater.go
Normal file
22
internal/provider/slickvpn/updater/updater.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
)
|
||||
|
||||
type Updater struct {
|
||||
client *http.Client
|
||||
parallelResolver common.ParallelResolver
|
||||
warner common.Warner
|
||||
}
|
||||
|
||||
func New(client *http.Client, warner common.Warner,
|
||||
parallelResolver common.ParallelResolver) *Updater {
|
||||
return &Updater{
|
||||
client: client,
|
||||
parallelResolver: parallelResolver,
|
||||
warner: warner,
|
||||
}
|
||||
}
|
||||
181
internal/provider/slickvpn/updater/website.go
Normal file
181
internal/provider/slickvpn/updater/website.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
htmlutils "github.com/qdm12/gluetun/internal/updater/html"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func fetchServers(ctx context.Context, client *http.Client) (
|
||||
hostToData map[string]serverData, err error) {
|
||||
rootNode, err := fetchHTML(ctx, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching HTML code: %w", err)
|
||||
}
|
||||
|
||||
hostToData, err = parseHTML(rootNode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing HTML: %w", err)
|
||||
}
|
||||
|
||||
return hostToData, nil
|
||||
}
|
||||
|
||||
var ErrHTTPStatusCode = errors.New("HTTP status code is not OK")
|
||||
|
||||
func fetchHTML(ctx context.Context, client *http.Client) (rootNode *html.Node, err error) {
|
||||
const url = "https://www.slickvpn.com/locations/"
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d %s",
|
||||
ErrHTTPStatusCode, response.StatusCode, response.Status)
|
||||
}
|
||||
|
||||
rootNode, err = html.Parse(response.Body)
|
||||
if err != nil {
|
||||
_ = response.Body.Close()
|
||||
return nil, fmt.Errorf("parsing HTML code: %w", err)
|
||||
}
|
||||
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("closing response body: %w", err)
|
||||
}
|
||||
|
||||
return rootNode, nil
|
||||
}
|
||||
|
||||
type serverData struct {
|
||||
ovpnURL string
|
||||
country string
|
||||
region string
|
||||
city string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrLocationTableNotFound = errors.New("HTML location table node not found")
|
||||
ErrTbodyNotFound = errors.New("HTML tbody node not found")
|
||||
ErrExtractOpenVPNURL = errors.New("failed extracting OpenVPN URL")
|
||||
)
|
||||
|
||||
func parseHTML(rootNode *html.Node) (hostToData map[string]serverData, err error) {
|
||||
locationTableNode := htmlutils.BFS(rootNode, matchLocationTable)
|
||||
if locationTableNode == nil {
|
||||
return nil, htmlutils.WrapError(ErrLocationTableNotFound, rootNode)
|
||||
}
|
||||
|
||||
tBodyNode := htmlutils.BFS(locationTableNode, matchTbody)
|
||||
if tBodyNode == nil {
|
||||
return nil, htmlutils.WrapError(ErrTbodyNotFound, rootNode)
|
||||
}
|
||||
|
||||
rowNodes := htmlutils.DirectChildren(tBodyNode, matchTr)
|
||||
hostToData = make(map[string]serverData, len(rowNodes))
|
||||
|
||||
for _, rowNode := range rowNodes {
|
||||
hostname, data, err := parseRowNode(rowNode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing row node: %w", err)
|
||||
}
|
||||
hostToData[hostname] = data
|
||||
}
|
||||
|
||||
return hostToData, nil
|
||||
}
|
||||
|
||||
func parseRowNode(rowNode *html.Node) (hostname string, data serverData, err error) {
|
||||
columnIndex := 0
|
||||
const (
|
||||
columnIndexContinent = 0
|
||||
columnIndexCountry = 1
|
||||
columnIndexCity = 2
|
||||
columnIndexConfig = 3
|
||||
)
|
||||
for cellNode := rowNode.FirstChild; cellNode != nil; cellNode = cellNode.NextSibling {
|
||||
if cellNode.FirstChild == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch columnIndex {
|
||||
case columnIndexContinent:
|
||||
data.region = cellNode.FirstChild.Data
|
||||
case columnIndexCountry:
|
||||
data.country = cellNode.FirstChild.Data
|
||||
case columnIndexCity:
|
||||
data.city = cellNode.FirstChild.Data
|
||||
case columnIndexConfig:
|
||||
linkNodes := htmlutils.DirectChildren(cellNode, matchA)
|
||||
for _, linkNode := range linkNodes {
|
||||
if linkNode.FirstChild.Data != "OpenVPN" {
|
||||
continue
|
||||
}
|
||||
|
||||
data.ovpnURL = htmlutils.Attribute(linkNode, "href")
|
||||
if data.ovpnURL == "" {
|
||||
return "", data, htmlutils.WrapError(ErrExtractOpenVPNURL, linkNode)
|
||||
}
|
||||
|
||||
hostname, err = extractHostnameFromURL(data.ovpnURL)
|
||||
if err != nil {
|
||||
return "", data, fmt.Errorf("extracting hostname from url: %w", err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
columnIndex++
|
||||
if columnIndex == columnIndexConfig+1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return hostname, data, nil
|
||||
}
|
||||
|
||||
func matchLocationTable(rootNode *html.Node) (match bool) {
|
||||
return htmlutils.MatchID("location-table")(rootNode)
|
||||
}
|
||||
|
||||
func matchTbody(locationTableNode *html.Node) (match bool) {
|
||||
return htmlutils.MatchData("tbody")(locationTableNode)
|
||||
}
|
||||
|
||||
func matchTr(tbodyNode *html.Node) (match bool) {
|
||||
return htmlutils.MatchData("tr")(tbodyNode)
|
||||
}
|
||||
|
||||
func matchA(cellNode *html.Node) (match bool) {
|
||||
return htmlutils.MatchData("a")(cellNode)
|
||||
}
|
||||
|
||||
var serverNameRegex = regexp.MustCompile(`^.+\/(?P<serverName>.+)\.ovpn$`)
|
||||
|
||||
var (
|
||||
ErrExtractHostnameFromURL = errors.New("cannot extract hostname from url")
|
||||
)
|
||||
|
||||
func extractHostnameFromURL(url string) (hostname string, err error) {
|
||||
matches := serverNameRegex.FindStringSubmatch(url)
|
||||
const minMatches = 2
|
||||
if len(matches) < minMatches {
|
||||
return "", fmt.Errorf("%w: %s has less than 2 matches for %s",
|
||||
ErrExtractHostnameFromURL, url, serverNameRegex)
|
||||
}
|
||||
hostname = matches[1]
|
||||
return hostname, nil
|
||||
}
|
||||
337
internal/provider/slickvpn/updater/website_test.go
Normal file
337
internal/provider/slickvpn/updater/website_test.go
Normal file
@@ -0,0 +1,337 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return f(r)
|
||||
}
|
||||
|
||||
func Test_fetchServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
ctx context.Context
|
||||
responseStatus int
|
||||
responseBody io.ReadCloser
|
||||
hostToData map[string]serverData
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"context canceled": {
|
||||
ctx: canceledCtx,
|
||||
errWrapped: context.Canceled,
|
||||
errMessage: `fetching HTML code: Get "https://www.slickvpn.com/locations/": context canceled`,
|
||||
},
|
||||
"success": {
|
||||
ctx: context.Background(),
|
||||
responseStatus: http.StatusOK,
|
||||
//nolint:lll
|
||||
responseBody: ioutil.NopCloser(strings.NewReader(`
|
||||
<div>
|
||||
<table id="location-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>South America</td>
|
||||
<td>Chile</td>
|
||||
<td>Vina del Mar</td>
|
||||
<td> <a
|
||||
href="https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kna1.slickvpn.com.ovpn">OpenVPN</a>
|
||||
| <a
|
||||
href="https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/viscosity/gw1.kna1.slickvpn.com_viscosity.ovpn">Viscosity</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`)),
|
||||
hostToData: map[string]serverData{
|
||||
"gw1.kna1.slickvpn.com": {
|
||||
ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kna1.slickvpn.com.ovpn", //nolint:lll
|
||||
country: "Chile",
|
||||
region: "South America",
|
||||
city: "Vina del Mar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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://www.slickvpn.com/locations/")
|
||||
|
||||
ctxErr := r.Context().Err()
|
||||
if ctxErr != nil {
|
||||
return nil, ctxErr
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Status: http.StatusText(testCase.responseStatus),
|
||||
Body: testCase.responseBody,
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
hostToData, err := fetchServers(testCase.ctx, client)
|
||||
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
assert.Equal(t, testCase.hostToData, hostToData)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fetchHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
ctx context.Context
|
||||
responseStatus int
|
||||
responseBody io.ReadCloser
|
||||
rootNode *html.Node
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"context canceled": {
|
||||
ctx: canceledCtx,
|
||||
errWrapped: context.Canceled,
|
||||
errMessage: `Get "https://www.slickvpn.com/locations/": context canceled`,
|
||||
},
|
||||
"response status not ok": {
|
||||
ctx: context.Background(),
|
||||
responseStatus: http.StatusNotFound,
|
||||
errWrapped: ErrHTTPStatusCode,
|
||||
errMessage: `HTTP status code is not OK: 404 Not Found`,
|
||||
},
|
||||
"success": {
|
||||
ctx: context.Background(),
|
||||
responseStatus: http.StatusOK,
|
||||
rootNode: parseTestHTML(t, "some body"),
|
||||
responseBody: ioutil.NopCloser(strings.NewReader("some body")),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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://www.slickvpn.com/locations/")
|
||||
|
||||
ctxErr := r.Context().Err()
|
||||
if ctxErr != nil {
|
||||
return nil, ctxErr
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: testCase.responseStatus,
|
||||
Status: http.StatusText(testCase.responseStatus),
|
||||
Body: testCase.responseBody,
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
rootNode, err := fetchHTML(testCase.ctx, client)
|
||||
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
assert.Equal(t, testCase.rootNode, rootNode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
rootNode *html.Node
|
||||
hostToData map[string]serverData
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"empty html": {
|
||||
rootNode: parseTestHTML(t, ""),
|
||||
errWrapped: ErrLocationTableNotFound,
|
||||
errMessage: `HTML location table node not found: in HTML code: <html><head></head><body></body></html>`,
|
||||
},
|
||||
"test data": {
|
||||
rootNode: parseTestDataIndexHTML(t),
|
||||
//nolint:lll
|
||||
hostToData: map[string]serverData{
|
||||
"gw1.ams1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ams1.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw1.ams2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ams2.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw1.ams3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ams3.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw1.ams4.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ams4.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw1.arn1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.arn1.slickvpn.com.ovpn", country: "Sweden", region: "Europe", city: "Stockholm"},
|
||||
"gw1.arn3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.arn3.slickvpn.com.ovpn", country: "Sweden", region: "Europe", city: "Stockholm"},
|
||||
"gw1.ath1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ath1.slickvpn.com.ovpn", country: "Greece", region: "Europe", city: "Athens"},
|
||||
"gw1.atl1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.atl1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Atlanta"},
|
||||
"gw1.atl3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.atl3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Atlanta"},
|
||||
"gw1.beg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.beg1.slickvpn.com.ovpn", country: "Serbia", region: "Europe", city: "Belgrade"},
|
||||
"gw1.bkk1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bkk1.slickvpn.com.ovpn", country: "Thailand", region: "Asia", city: "Bangkok"},
|
||||
"gw1.blr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.blr1.slickvpn.com.ovpn", country: "India", region: "Asia", city: "Bangalore"},
|
||||
"gw1.bne1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bne1.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Brisbane"},
|
||||
"gw1.bom1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bom1.slickvpn.com.ovpn", country: "India", region: "Asia", city: "Mumbai"},
|
||||
"gw1.bos1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bos1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Boston"},
|
||||
"gw1.bud1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.bud1.slickvpn.com.ovpn", country: "Hungary", region: "Europe", city: "Budapest"},
|
||||
"gw1.buf1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.buf1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Buffalo"},
|
||||
"gw1.buh2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.buh2.slickvpn.com.ovpn", country: "Romania", region: "Europe", city: "Bucharest"},
|
||||
"gw1.cdg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cdg1.slickvpn.com.ovpn", country: "France", region: "Europe", city: "Paris"},
|
||||
"gw1.cgk1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cgk1.slickvpn.com.ovpn", country: "Indonesia", region: "Asia", city: "Jakarta"},
|
||||
"gw1.cmh1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cmh1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Columbus"},
|
||||
"gw1.cph1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cph1.slickvpn.com.ovpn", country: "Denmark", region: "Europe", city: "Copenhagen"},
|
||||
"gw1.cvt1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.cvt1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Coventry"},
|
||||
"gw1.dbq1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.dbq1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dubuque"},
|
||||
"gw1.den1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.den1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Denver"},
|
||||
"gw1.dfw2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.dfw2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dallas"},
|
||||
"gw1.dfw3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.dfw3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dallas"},
|
||||
"gw1.dub1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.dub1.slickvpn.com.ovpn", country: "Ireland", region: "Europe", city: "Dublin"},
|
||||
"gw1.ewr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ewr1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Newark"},
|
||||
"gw1.fra1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.fra1.slickvpn.com.ovpn", country: "Germany", region: "Europe", city: "Frankfurt"},
|
||||
"gw1.fra2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.fra2.slickvpn.com.ovpn", country: "Germany", region: "Europe", city: "Frankfurt"},
|
||||
"gw1.gru2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.gru2.slickvpn.com.ovpn", country: "Brazil", region: "South America", city: "Sao Paulo"},
|
||||
"gw1.grz1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.grz1.slickvpn.com.ovpn", country: "Austria", region: "Europe", city: "Graz"},
|
||||
"gw1.had2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.had2.slickvpn.com.ovpn", country: "Sweden", region: "Europe", city: "Halmstad"},
|
||||
"gw1.hkg2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.hkg2.slickvpn.com.ovpn", country: "Hong Kong", region: "Asia", city: "Hong Kong"},
|
||||
"gw1.iad1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.iad1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Washington"},
|
||||
"gw1.iev1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.iev1.slickvpn.com.ovpn", country: "Ukraine", region: "Europe", city: "Kiev"},
|
||||
"gw1.iom1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.iom1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Isle Of Man"},
|
||||
"gw1.kiv1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kiv1.slickvpn.com.ovpn", country: "Moldova", region: "Europe", city: "Chisinau"},
|
||||
"gw1.kna1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kna1.slickvpn.com.ovpn", country: "Chile", region: "South America", city: "Vina del Mar"},
|
||||
"gw1.kul1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.kul1.slickvpn.com.ovpn", country: "Malaysia", region: "Asia", city: "Kuala Lumpur"},
|
||||
"gw1.las1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.las1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Las Vegas"},
|
||||
"gw1.lax1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lax1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Los Angeles"},
|
||||
"gw1.lax2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lax2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Los Angeles"},
|
||||
"gw1.led1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.led1.slickvpn.com.ovpn", country: "Russian Federation", region: "Europe", city: "St Petersburg"},
|
||||
"gw1.lga1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lga1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "New York"},
|
||||
"gw1.lga2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lga2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "New York"},
|
||||
"gw1.lhr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lhr1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw1.lhr2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lhr2.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw1.lil1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lil1.slickvpn.com.ovpn", country: "France", region: "Europe", city: "Lille"},
|
||||
"gw1.lju1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.lju1.slickvpn.com.ovpn", country: "Slovenia", region: "Europe", city: "Ljubljana"},
|
||||
"gw1.mad1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mad1.slickvpn.com.ovpn", country: "Spain", region: "Europe", city: "Madrid"},
|
||||
"gw1.man2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.man2.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Manchester"},
|
||||
"gw1.mci2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mci2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Kansas City"},
|
||||
"gw1.mrn1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mrn1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Morganton"},
|
||||
"gw1.mxp1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mxp1.slickvpn.com.ovpn", country: "Italy", region: "Europe", city: "Milan"},
|
||||
"gw1.mxp2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.mxp2.slickvpn.com.ovpn", country: "Italy", region: "Europe", city: "Milan"},
|
||||
"gw1.nrt1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.nrt1.slickvpn.com.ovpn", country: "Japan", region: "Asia", city: "Tokyo"},
|
||||
"gw1.nue1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.nue1.slickvpn.com.ovpn", country: "Germany", region: "Europe", city: "Nürnberg"},
|
||||
"gw1.ord3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ord3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Chicago"},
|
||||
"gw1.ord4.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ord4.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Chicago"},
|
||||
"gw1.ost2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.ost2.slickvpn.com.ovpn", country: "Belgium", region: "Europe", city: "Ostend"},
|
||||
"gw1.pao1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.pao1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Palo Alto"},
|
||||
"gw1.phx2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.phx2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Phoenix"},
|
||||
"gw1.prg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.prg1.slickvpn.com.ovpn", country: "Czech Republic", region: "Europe", city: "Prague"},
|
||||
"gw1.prg2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.prg2.slickvpn.com.ovpn", country: "Czech Republic", region: "Europe", city: "Prague"},
|
||||
"gw1.rcs1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.rcs1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Rochester"},
|
||||
"gw1.rkv1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.rkv1.slickvpn.com.ovpn", country: "Iceland", region: "Europe", city: "Reykjavik"},
|
||||
"gw1.san1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.san1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "San Diego"},
|
||||
"gw1.sea1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sea1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Seattle"},
|
||||
"gw1.sea2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sea2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Seattle"},
|
||||
"gw1.sin1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sin1.slickvpn.com.ovpn", country: "Singapore", region: "Asia", city: "Singapore"},
|
||||
"gw1.sin2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sin2.slickvpn.com.ovpn", country: "Singapore", region: "Asia", city: "Singapore"},
|
||||
"gw1.sjc2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sjc2.slickvpn.com.ovpn", country: "United States", region: "North America", city: "San Jose"},
|
||||
"gw1.skg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.skg1.slickvpn.com.ovpn", country: "Greece", region: "Europe", city: "Thessaloniki"},
|
||||
"gw1.sou1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.sou1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Eastleigh near Southampton"},
|
||||
"gw1.stl1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.stl1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "St Louis"},
|
||||
"gw1.svo1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.svo1.slickvpn.com.ovpn", country: "Russian Federation", region: "Europe", city: "Moscow"},
|
||||
"gw1.svo2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.svo2.slickvpn.com.ovpn", country: "Russian Federation", region: "Europe", city: "Moscow"},
|
||||
"gw1.syd1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.syd1.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Sydney"},
|
||||
"gw1.syd2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.syd2.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Sydney"},
|
||||
"gw1.tll1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.tll1.slickvpn.com.ovpn", country: "Estonia", region: "Europe", city: "Tallinn"},
|
||||
"gw1.tlv2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.tlv2.slickvpn.com.ovpn", country: "Israel", region: "Asia", city: "Tel Aviv Yafo"},
|
||||
"gw1.tpa1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.tpa1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Tampa"},
|
||||
"gw1.trf1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.trf1.slickvpn.com.ovpn", country: "Norway", region: "Europe", city: "Torp"},
|
||||
"gw1.waw1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.waw1.slickvpn.com.ovpn", country: "Poland", region: "Europe", city: "Warsaw"},
|
||||
"gw1.yei1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yei1.slickvpn.com.ovpn", country: "Turkey", region: "Asia", city: "Bursa"},
|
||||
"gw1.yul1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yul1.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Montreal"},
|
||||
"gw1.yul2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yul2.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Montreal"},
|
||||
"gw1.yvr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yvr1.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Vancouver"},
|
||||
"gw1.yyz1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.yyz1.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Toronto"},
|
||||
"gw1.zrh1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw1.zrh1.slickvpn.com.ovpn", country: "Switzerland", region: "Europe", city: "Zurich"},
|
||||
"gw2.ams3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.ams3.slickvpn.com.ovpn", country: "Netherlands", region: "Europe", city: "Amsterdam"},
|
||||
"gw2.atl3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.atl3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Atlanta"},
|
||||
"gw2.bcn2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.bcn2.slickvpn.com.ovpn", country: "Spain", region: "Europe", city: "Barcelona"},
|
||||
"gw2.clt1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.clt1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Charlotte"},
|
||||
"gw2.dfw3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.dfw3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dallas"},
|
||||
"gw2.ewr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.ewr1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Newark"},
|
||||
"gw2.fra1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.fra1.slickvpn.com.ovpn", country: "Germany", region: "Europe", city: "Frankfurt"},
|
||||
"gw2.hou1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.hou1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Houston"},
|
||||
"gw2.iad1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.iad1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Washington"},
|
||||
"gw2.lhr2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.lhr2.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw2.mel1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.mel1.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Melbourne"},
|
||||
"gw2.mia3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.mia3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Miami"},
|
||||
"gw2.mia4.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.mia4.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Miami"},
|
||||
"gw2.mxp2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.mxp2.slickvpn.com.ovpn", country: "Italy", region: "Europe", city: "Milan"},
|
||||
"gw2.ord1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.ord1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Chicago"},
|
||||
"gw2.ost2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.ost2.slickvpn.com.ovpn", country: "Belgium", region: "Europe", city: "Ostend"},
|
||||
"gw2.pao1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.pao1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Palo Alto"},
|
||||
"gw2.prg1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.prg1.slickvpn.com.ovpn", country: "Czech Republic", region: "Europe", city: "Prague"},
|
||||
"gw2.pty1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.pty1.slickvpn.com.ovpn", country: "Panama", region: "North America", city: "Panama City"},
|
||||
"gw2.sin2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.sin2.slickvpn.com.ovpn", country: "Singapore", region: "Asia", city: "Singapore"},
|
||||
"gw2.slc1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.slc1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Salt Lake City"},
|
||||
"gw2.syd2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.syd2.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Sydney"},
|
||||
"gw2.tpe1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.tpe1.slickvpn.com.ovpn", country: "Taiwan", region: "Asia", city: "Taipei"},
|
||||
"gw2.yul2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.yul2.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Montreal"},
|
||||
"gw2.yyz1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw2.yyz1.slickvpn.com.ovpn", country: "Canada", region: "North America", city: "Toronto"},
|
||||
"gw3.dfw3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.dfw3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Dallas"},
|
||||
"gw3.ewr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.ewr1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Newark"},
|
||||
"gw3.iad1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.iad1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Washington"},
|
||||
"gw3.lax3.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.lax3.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Los Angeles"},
|
||||
"gw3.lhr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.lhr1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw3.lhr2.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.lhr2.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
"gw3.pao1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.pao1.slickvpn.com.ovpn", country: "United States", region: "North America", city: "Palo Alto"},
|
||||
"gw3.per1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.per1.slickvpn.com.ovpn", country: "Australia", region: "Oceania", city: "Perth"},
|
||||
"gw3.sou1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw3.sou1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "Eastleigh near Southampton"},
|
||||
"gw4.lhr1.slickvpn.com": {ovpnURL: "https://www.slickvpn.com/wp-content/themes/slickvpn-theme/vpn-configs/ovpn/gw4.lhr1.slickvpn.com.ovpn", country: "United Kingdom", region: "Europe", city: "London"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hostToData, err := parseHTML(testCase.rootNode)
|
||||
|
||||
assert.Equal(t, testCase.hostToData, hostToData)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
12
internal/updater/html/attribute.go
Normal file
12
internal/updater/html/attribute.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package html
|
||||
|
||||
import "golang.org/x/net/html"
|
||||
|
||||
func Attribute(node *html.Node, key string) (value string) {
|
||||
for _, attribute := range node.Attr {
|
||||
if attribute.Key == key {
|
||||
return attribute.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
43
internal/updater/html/bfs.go
Normal file
43
internal/updater/html/bfs.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// BFS returns the node matching the match function and nil
|
||||
// if no node is found.
|
||||
func BFS(rootNode *html.Node, match MatchFunc) (node *html.Node) {
|
||||
visited := make(map[*html.Node]struct{})
|
||||
queue := list.New()
|
||||
_ = queue.PushBack(rootNode)
|
||||
|
||||
for queue.Len() > 0 {
|
||||
listElement := queue.Front()
|
||||
node, ok := queue.Remove(listElement).(*html.Node)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("linked list has bad type %T", listElement.Value))
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := visited[node]; ok {
|
||||
continue
|
||||
}
|
||||
visited[node] = struct{}{}
|
||||
|
||||
if match(node) {
|
||||
return node
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
_ = queue.PushBack(child)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
22
internal/updater/html/css.go
Normal file
22
internal/updater/html/css.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func HasClassStrings(node *html.Node, classStrings ...string) (match bool) {
|
||||
targetClasses := make(map[string]struct{}, len(classStrings))
|
||||
for _, classString := range classStrings {
|
||||
targetClasses[classString] = struct{}{}
|
||||
}
|
||||
|
||||
classAttribute := Attribute(node, "class")
|
||||
classes := strings.Fields(classAttribute)
|
||||
for _, class := range classes {
|
||||
delete(targetClasses, class)
|
||||
}
|
||||
|
||||
return len(targetClasses) == 0
|
||||
}
|
||||
27
internal/updater/html/errors.go
Normal file
27
internal/updater/html/errors.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func WrapError(sentinelError error, node *html.Node) error {
|
||||
return fmt.Errorf("%w: in HTML code: %s",
|
||||
sentinelError, mustRenderHTML(node))
|
||||
}
|
||||
|
||||
func WrapWarning(warning string, node *html.Node) string {
|
||||
return fmt.Sprintf("%s: in HTML code: %s",
|
||||
warning, mustRenderHTML(node))
|
||||
}
|
||||
|
||||
func mustRenderHTML(node *html.Node) (rendered string) {
|
||||
stringBuffer := bytes.NewBufferString("")
|
||||
err := html.Render(stringBuffer, node)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return stringBuffer.String()
|
||||
}
|
||||
43
internal/updater/html/match.go
Normal file
43
internal/updater/html/match.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type MatchFunc func(node *html.Node) (match bool)
|
||||
|
||||
func MatchID(id string) MatchFunc {
|
||||
return func(node *html.Node) (match bool) {
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return Attribute(node, "id") == id
|
||||
}
|
||||
}
|
||||
|
||||
func MatchData(data string) MatchFunc {
|
||||
return func(node *html.Node) (match bool) {
|
||||
return node != nil && node.Type == html.ElementNode && node.Data == data
|
||||
}
|
||||
}
|
||||
|
||||
func DirectChild(parent *html.Node,
|
||||
matchFunc MatchFunc) (child *html.Node) {
|
||||
for child := parent.FirstChild; child != nil; child = child.NextSibling {
|
||||
if matchFunc(child) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DirectChildren(parent *html.Node,
|
||||
matchFunc MatchFunc) (children []*html.Node) {
|
||||
for child := parent.FirstChild; child != nil; child = child.NextSibling {
|
||||
if matchFunc(child) {
|
||||
children = append(children, child)
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
@@ -76,6 +76,7 @@ func ExtractIPs(b []byte) (ips []net.IP, err error) {
|
||||
func extractRemoteHosts(content []byte, rejectIP, rejectDomain bool) (hosts []string) {
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "remote ") {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
// FetchMultiFiles fetches multiple Openvpn files in parallel and
|
||||
// parses them to extract each of their host. A mapping from host to
|
||||
// URL is returned.
|
||||
func FetchMultiFiles(ctx context.Context, client *http.Client, urls []string) (
|
||||
hostToURL map[string]string, err error) {
|
||||
func FetchMultiFiles(ctx context.Context, client *http.Client, urls []string,
|
||||
failEarly bool) (hostToURL map[string]string, errors []error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
@@ -22,14 +22,14 @@ func FetchMultiFiles(ctx context.Context, client *http.Client, urls []string) (
|
||||
|
||||
results := make(chan Result)
|
||||
defer close(results)
|
||||
errors := make(chan error)
|
||||
defer close(errors)
|
||||
errorsCh := make(chan error)
|
||||
defer close(errorsCh)
|
||||
|
||||
for _, url := range urls {
|
||||
go func(url string) {
|
||||
host, err := FetchFile(ctx, client, url)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
errorsCh <- err
|
||||
return
|
||||
}
|
||||
results <- Result{
|
||||
@@ -41,19 +41,26 @@ func FetchMultiFiles(ctx context.Context, client *http.Client, urls []string) (
|
||||
|
||||
for range urls {
|
||||
select {
|
||||
case newErr := <-errors:
|
||||
if err == nil { // only assign to the first error
|
||||
err = newErr
|
||||
cancel() // stop other operations, this will trigger other errors we ignore
|
||||
}
|
||||
case result := <-results:
|
||||
hostToURL[result.host] = result.url
|
||||
case err := <-errorsCh:
|
||||
if !failEarly {
|
||||
errors = append(errors, err)
|
||||
break
|
||||
}
|
||||
|
||||
if len(errors) == 0 {
|
||||
errors = []error{err} // keep only the first error
|
||||
// stop other operations, this will trigger other errors we ignore
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(errors) > 0 && failEarly {
|
||||
// we don't care about the result found
|
||||
return nil, errors
|
||||
}
|
||||
|
||||
return hostToURL, nil
|
||||
return hostToURL, errors
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user