Skip to content

Commit d8c8238

Browse files
committed
wip2
1 parent 9475f9c commit d8c8238

29 files changed

Lines changed: 13328 additions & 1322 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ require (
1515
github.com/mdlayher/netlink v1.9.0
1616
github.com/pelletier/go-toml/v2 v2.2.4
1717
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
18-
github.com/qdm12/gluetun-servers v0.1.0
18+
github.com/qdm12/gluetun-servers v0.1.1-0.20260623154528-d1b2c9a591ba
1919
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978
2020
github.com/qdm12/gosettings v0.4.4
2121
github.com/qdm12/goshutdown v0.3.0

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbV
7878
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
7979
github.com/qdm12/gluetun-servers v0.1.0 h1:w9JLghKZwI0Gzpp9p5rNANgEYUUZ1dxdxsG6NKIojaY=
8080
github.com/qdm12/gluetun-servers v0.1.0/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8=
81+
github.com/qdm12/gluetun-servers v0.1.1-0.20260623154528-d1b2c9a591ba/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8=
8182
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
8283
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
8384
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=

internal/configuration/settings/serverselection.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,6 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
9191
*ss = nordvpnRetroRegion(*ss, filterChoices.Regions, filterChoices.Countries)
9292
case providers.Surfshark:
9393
*ss = surfsharkRetroRegion(*ss)
94-
case providers.Purevpn:
95-
// Keep parsing SERVER_REGIONS for retro-compatibility, but
96-
// do not apply it to PureVPN filtering.
97-
ss.Regions = nil
9894
}
9995

10096
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner)

internal/models/server.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ type Server struct {
2424
Hostname string `json:"hostname,omitempty"`
2525
TCP bool `json:"tcp,omitempty"`
2626
UDP bool `json:"udp,omitempty"`
27-
OpenVPNTCPPorts []uint16 `json:"openvpn_tcp_ports,omitempty"`
28-
OpenVPNUDPPorts []uint16 `json:"openvpn_udp_ports,omitempty"`
27+
TCPPorts []uint16 `json:"tcp_ports,omitempty"`
28+
UDPPorts []uint16 `json:"udp_ports,omitempty"`
2929
OvpnX509 string `json:"x509,omitempty"`
3030
RetroLoc string `json:"retroloc,omitempty"` // TODO remove in v4
3131
MultiHop bool `json:"multihop,omitempty"`
@@ -61,18 +61,18 @@ func (s *Server) HasMinimumInformation() (err error) {
6161

6262
func (s *Server) Equal(other Server) (equal bool) {
6363
if !comparablesAreEqualNoOrder(s.IPs, other.IPs) ||
64-
!comparablesAreEqualNoOrder(s.OpenVPNTCPPorts, other.OpenVPNTCPPorts) ||
65-
!comparablesAreEqualNoOrder(s.OpenVPNUDPPorts, other.OpenVPNUDPPorts) {
64+
!comparablesAreEqualNoOrder(s.TCPPorts, other.TCPPorts) ||
65+
!comparablesAreEqualNoOrder(s.UDPPorts, other.UDPPorts) {
6666
return false
6767
}
6868

6969
serverCopy := *s
7070
serverCopy.IPs = nil
71-
serverCopy.OpenVPNTCPPorts = nil
72-
serverCopy.OpenVPNUDPPorts = nil
71+
serverCopy.TCPPorts = nil
72+
serverCopy.UDPPorts = nil
7373
other.IPs = nil
74-
other.OpenVPNTCPPorts = nil
75-
other.OpenVPNUDPPorts = nil
74+
other.TCPPorts = nil
75+
other.UDPPorts = nil
7676
return reflect.DeepEqual(serverCopy, other)
7777
}
7878

internal/models/server_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ func Test_Server_Equal(t *testing.T) {
5454
Hostname: "hostname",
5555
TCP: true,
5656
UDP: true,
57-
OpenVPNTCPPorts: []uint16{123, 456},
58-
OpenVPNUDPPorts: []uint16{678, 901},
57+
TCPPorts: []uint16{123, 456},
58+
UDPPorts: []uint16{678, 901},
5959
OvpnX509: "x509",
6060
RetroLoc: "retroloc",
6161
MultiHop: true,
@@ -82,8 +82,8 @@ func Test_Server_Equal(t *testing.T) {
8282
Hostname: "hostname",
8383
TCP: true,
8484
UDP: true,
85-
OpenVPNTCPPorts: []uint16{456, 123},
86-
OpenVPNUDPPorts: []uint16{901, 678},
85+
TCPPorts: []uint16{456, 123},
86+
UDPPorts: []uint16{901, 678},
8787
OvpnX509: "x509",
8888
RetroLoc: "retroloc",
8989
MultiHop: true,

internal/provider/providers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
7272
providers.PrivateInternetAccess: privateinternetaccess.New(storage, timeNow, client),
7373
providers.Privatevpn: privatevpn.New(storage, unzipper, updaterWarner, parallelResolver),
7474
providers.Protonvpn: protonvpn.New(storage, client, updaterWarner, *credentials.ProtonEmail, *credentials.ProtonPassword),
75-
providers.Purevpn: purevpn.New(storage, client, ipFetcher, unzipper, updaterWarner, parallelResolver),
75+
providers.Purevpn: purevpn.New(storage, client, unzipper, updaterWarner, parallelResolver),
7676
providers.SlickVPN: slickvpn.New(storage, client, updaterWarner, parallelResolver),
7777
providers.Surfshark: surfshark.New(storage, client, unzipper, updaterWarner, parallelResolver),
7878
providers.Torguard: torguard.New(storage, unzipper, updaterWarner, parallelResolver),

internal/provider/purevpn/provider.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ type Provider struct {
1616
}
1717

1818
func New(storage common.Storage, client *http.Client,
19-
ipFetcher common.IPFetcher, unzipper common.Unzipper,
20-
updaterWarner common.Warner, parallelResolver common.ParallelResolver,
19+
unzipper common.Unzipper, updaterWarner common.Warner,
20+
parallelResolver common.ParallelResolver,
2121
) *Provider {
2222
return &Provider{
2323
storage: storage,
2424
connPicker: utils.NewConnectionPicker(),
25-
Fetcher: updater.New(client, ipFetcher, unzipper, updaterWarner, parallelResolver),
25+
Fetcher: updater.New(client, unzipper, updaterWarner, parallelResolver),
2626
}
2727
}
2828

internal/provider/purevpn/updater/deb.go

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"compress/gzip"
77
"encoding/binary"
88
"encoding/json"
9+
"errors"
910
"fmt"
1011
"io"
1112
"strconv"
@@ -15,40 +16,40 @@ import (
1516
"github.com/ulikunitz/xz"
1617
)
1718

18-
const pureVPNAsarPath = "opt/PureVPN/resources/app.asar"
19-
2019
type debEntry struct {
2120
name string
2221
data []byte
2322
}
2423

25-
func extractAsarFromDeb(debBytes []byte) (asarContent []byte, err error) {
26-
entries, err := parseArArchive(debBytes)
24+
func extractAsarFromDeb(deb []byte) (asarContent []byte, err error) {
25+
entries, err := parseArArchive(deb)
2726
if err != nil {
2827
return nil, fmt.Errorf("parsing .deb ar archive: %w", err)
2928
}
3029

3130
var dataTarName string
3231
var dataTarCompressed []byte
3332
for _, entry := range entries {
34-
if strings.HasPrefix(entry.name, "data.tar") {
33+
if strings.HasPrefix(entry.name, "data.tar") && len(entry.data) > 0 {
3534
dataTarName = entry.name
3635
dataTarCompressed = entry.data
3736
break
3837
}
3938
}
40-
if len(dataTarCompressed) == 0 {
41-
return nil, fmt.Errorf("data.tar archive not found in .deb")
39+
40+
if dataTarName == "" {
41+
return nil, errors.New("data.tar archive not found in .deb")
4242
}
4343

4444
dataTar, err := decompressDataTar(dataTarName, dataTarCompressed)
4545
if err != nil {
4646
return nil, fmt.Errorf("decompressing %s: %w", dataTarName, err)
4747
}
4848

49-
asarContent, err = extractFileFromTar(dataTar, pureVPNAsarPath)
49+
const appAsarPath = "opt/PureVPN/resources/app.asar"
50+
asarContent, err = extractFileFromTar(dataTar, appAsarPath)
5051
if err != nil {
51-
return nil, fmt.Errorf("extracting %s from tar: %w", pureVPNAsarPath, err)
52+
return nil, fmt.Errorf("extracting %s from tar: %w", appAsarPath, err)
5253
}
5354

5455
return asarContent, nil
@@ -160,48 +161,45 @@ func extractFileFromTar(tarContent []byte, expectedPath string) (fileContent []b
160161
type asarNode struct {
161162
Files map[string]*asarNode `json:"files,omitempty"`
162163
Offset string `json:"offset,omitempty"`
163-
Size int `json:"size,omitempty"`
164+
Size uint `json:"size,omitempty"`
164165
}
165166

166167
type asarHeader struct {
167168
Files map[string]*asarNode `json:"files"`
168169
}
169170

170171
func extractFileFromAsar(asarContent []byte, targetPath string) (fileContent []byte, err error) {
171-
if len(asarContent) < 16 {
172+
const minAsarLength = 16
173+
if len(asarContent) < minAsarLength {
172174
return nil, fmt.Errorf("asar content too short: %d", len(asarContent))
173175
}
174176

175177
headerLength := int(binary.LittleEndian.Uint32(asarContent[12:16]))
176178
if headerLength <= 0 {
177179
return nil, fmt.Errorf("invalid asar header length: %d", headerLength)
178180
}
179-
if 16+headerLength > len(asarContent) {
181+
if minAsarLength+headerLength > len(asarContent) {
180182
return nil, fmt.Errorf("asar header length exceeds content length")
181183
}
182184

183185
headerContent := asarContent[16 : 16+headerLength]
184186
var header asarHeader
185187
if err := json.Unmarshal(headerContent, &header); err != nil {
186-
return nil, fmt.Errorf("unmarshalling asar header: %w", err)
188+
return nil, fmt.Errorf("json decoding asar header: %w", err)
187189
}
188190

189191
node, err := asarGetNode(header.Files, targetPath)
190192
if err != nil {
191-
return nil, err
193+
return nil, fmt.Errorf("getting asar node: %w", err)
192194
}
193195

194196
offset, err := strconv.Atoi(node.Offset)
195197
if err != nil {
196-
return nil, fmt.Errorf("parsing asar file offset %q for %q: %w", node.Offset, targetPath, err)
198+
return nil, fmt.Errorf("parsing asar node offset: %w", err)
197199
}
198-
if node.Size < 0 {
199-
return nil, fmt.Errorf("negative asar file size %d for %q", node.Size, targetPath)
200-
}
201-
202-
dataOffset := 16 + headerLength + offset
203-
dataEnd := dataOffset + node.Size
204-
if dataOffset < 0 || dataEnd > len(asarContent) {
200+
dataOffset := minAsarLength + headerLength + offset
201+
dataEnd := dataOffset + int(node.Size) //nolint:gosec
202+
if dataEnd > len(asarContent) {
205203
return nil, fmt.Errorf("asar file %q exceeds content boundaries", targetPath)
206204
}
207205

@@ -217,7 +215,7 @@ func asarGetNode(files map[string]*asarNode, targetPath string) (node *asarNode,
217215
for i, segment := range segments {
218216
node = currentFiles[segment]
219217
if node == nil {
220-
return nil, fmt.Errorf("path %q not found in asar", targetPath)
218+
return nil, fmt.Errorf("segment %q from path %s not found in files map %v", segment, targetPath, currentFiles)
221219
}
222220
if i == len(segments)-1 {
223221
return node, nil

internal/provider/purevpn/updater/deburl.go

Lines changed: 11 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@ import (
55
"errors"
66
"fmt"
77
"io"
8-
"maps"
98
"net/http"
109
"net/url"
1110
"regexp"
12-
"slices"
13-
"sort"
14-
"strconv"
1511
"strings"
1612
)
1713

@@ -37,12 +33,11 @@ func fetchDebURL(ctx context.Context, client *http.Client) (debURL string, err e
3733
return "", fmt.Errorf("reading response body: %w", err)
3834
}
3935

40-
debURLs, err := extractDebURLs(string(html), url)
36+
debURL, err = extractDebURL(string(html), url)
4137
if err != nil {
4238
return "", fmt.Errorf("extracting .deb URLs from download page: %w", err)
4339
}
44-
45-
return findBestDebURL(debURLs), nil
40+
return debURL, nil
4641
}
4742

4843
func fetchURL(ctx context.Context, client *http.Client, rawURL string) (content []byte, err error) {
@@ -57,7 +52,7 @@ func fetchURL(ctx context.Context, client *http.Client, rawURL string) (content
5752
}
5853
defer response.Body.Close()
5954

60-
if response.StatusCode < http.StatusOK || response.StatusCode > 299 {
55+
if response.StatusCode != http.StatusOK {
6156
return nil, fmt.Errorf("HTTP status code %d", response.StatusCode)
6257
}
6358

@@ -73,16 +68,15 @@ var (
7368
hrefDebPattern = regexp.MustCompile(`href=["']([^"']+\.deb)["']`)
7469
)
7570

76-
func extractDebURLs(pageHTML, baseURL string) (debURLs []string, err error) {
71+
func extractDebURL(pageHTML, baseURL string) (debURL string, err error) {
7772
baseParsed, err := url.Parse(baseURL)
7873
if err != nil {
79-
return nil, fmt.Errorf("parsing base url %q: %w", baseURL, err)
74+
return "", fmt.Errorf("parsing base url %q: %w", baseURL, err)
8075
}
8176

82-
urlsSet := make(map[string]struct{})
83-
84-
for _, match := range debURLPattern.FindAllString(pageHTML, -1) {
85-
urlsSet[match] = struct{}{}
77+
debURL = debURLPattern.FindString(pageHTML)
78+
if debURL != "" {
79+
return debURL, nil
8680
}
8781

8882
for _, groups := range hrefDebPattern.FindAllStringSubmatch(pageHTML, -1) {
@@ -94,98 +88,9 @@ func extractDebURLs(pageHTML, baseURL string) (debURLs []string, err error) {
9488
if err != nil {
9589
continue
9690
}
97-
resolved := baseParsed.ResolveReference(href).String()
98-
urlsSet[resolved] = struct{}{}
99-
}
100-
101-
if len(urlsSet) == 0 {
102-
return nil, errors.New("no .deb URL found")
103-
}
104-
105-
return slices.Collect(maps.Keys(urlsSet)), nil
106-
}
107-
108-
func findBestDebURL(debURLs []string) (bestURL string) {
109-
type debCandidate struct {
110-
url string
111-
score int
112-
major uint16
113-
minor uint16
114-
patch uint16
115-
position uint
116-
}
117-
candidates := make([]debCandidate, len(debURLs))
118-
for i, debURL := range debURLs {
119-
score := scoreDebURL(debURL)
120-
major, minor, patch := parseSemverFromURL(debURL)
121-
candidates[i] = debCandidate{
122-
url: debURL,
123-
score: score,
124-
major: major,
125-
minor: minor,
126-
patch: patch,
127-
position: uint(i), //nolint:gosec
128-
}
129-
}
130-
131-
sort.Slice(candidates, func(i, j int) bool {
132-
left := candidates[i]
133-
right := candidates[j]
134-
if left.score != right.score {
135-
return left.score > right.score
136-
}
137-
if left.major != right.major {
138-
return left.major > right.major
139-
}
140-
if left.minor != right.minor {
141-
return left.minor > right.minor
142-
}
143-
if left.patch != right.patch {
144-
return left.patch > right.patch
145-
}
146-
if left.position != right.position {
147-
return left.position < right.position
148-
}
149-
return left.url < right.url
150-
})
151-
152-
return candidates[0].url
153-
}
154-
155-
func scoreDebURL(debURL string) (score int) {
156-
keywordToPoints := map[string]int{
157-
"purevpn": 40,
158-
"linux": 30,
159-
"gui": 20,
160-
"amd64": 20,
161-
"arm": -25,
162-
"aarch": -25,
163-
"i386": -25,
164-
"x86": -25,
165-
}
166-
167-
debURL = strings.ToLower(debURL)
168-
for keyword, points := range keywordToPoints {
169-
if strings.Contains(debURL, keyword) {
170-
score += points
171-
}
172-
}
173-
174-
return score
175-
}
176-
177-
var semverPattern = regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`)
178-
179-
func parseSemverFromURL(rawURL string) (major, minor, patch uint16) {
180-
match := semverPattern.FindStringSubmatch(rawURL)
181-
const expectedMatchesPlusOne = 4 // full match + 3 groups
182-
if len(match) != expectedMatchesPlusOne {
183-
return 0, 0, 0
91+
debURL = baseParsed.ResolveReference(href).String()
92+
return debURL, nil
18493
}
18594

186-
const base, bitSize = 10, 16
187-
majorUint64, _ := strconv.ParseUint(match[1], base, bitSize)
188-
minorUint64, _ := strconv.ParseUint(match[2], base, bitSize)
189-
patchUint64, _ := strconv.ParseUint(match[3], base, bitSize)
190-
return uint16(majorUint64), uint16(minorUint64), uint16(patchUint64)
95+
return "", errors.New("no .deb URL found")
19196
}

0 commit comments

Comments
 (0)