mirror of
https://github.com/qdm12/gluetun.git
synced 2026-07-25 11:56:24 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00d944e713 | |||
| beda1764b1 | |||
| 2210a0e9ad | |||
| f8a677a424 |
@@ -27,6 +27,7 @@ require (
|
|||||||
github.com/ulikunitz/xz v0.5.15
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||||
|
golang.org/x/mod v0.33.0
|
||||||
golang.org/x/net v0.51.0
|
golang.org/x/net v0.51.0
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/sys v0.42.0
|
||||||
golang.org/x/text v0.35.0
|
golang.org/x/text v0.35.0
|
||||||
@@ -58,7 +59,6 @@ require (
|
|||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/mod v0.33.0 // indirect
|
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ func (c *Cmder) Start(cmd *exec.Cmd) (
|
|||||||
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||||
waitError <-chan error, startErr error,
|
waitError <-chan error, startErr error,
|
||||||
) {
|
) {
|
||||||
stop := make(chan struct{})
|
|
||||||
stdoutReady := make(chan struct{})
|
stdoutReady := make(chan struct{})
|
||||||
stdoutLinesCh := make(chan string)
|
stdoutLinesCh := make(chan string)
|
||||||
stdoutDone := make(chan struct{})
|
stdoutDone := make(chan struct{})
|
||||||
@@ -33,22 +32,20 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
|
go streamToChannel(stdoutReady, stdoutDone, stdout, stdoutLinesCh)
|
||||||
|
|
||||||
stderr, err := cmd.StderrPipe()
|
stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
close(stop)
|
|
||||||
<-stdoutDone
|
<-stdoutDone
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
|
go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
|
||||||
|
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
_ = stderr.Close()
|
_ = stderr.Close()
|
||||||
close(stop)
|
|
||||||
<-stdoutDone
|
<-stdoutDone
|
||||||
<-stderrDone
|
<-stderrDone
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
@@ -57,19 +54,20 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
|||||||
waitErrorCh := make(chan error)
|
waitErrorCh := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
_ = stdout.Close()
|
|
||||||
_ = stderr.Close()
|
|
||||||
close(stop)
|
|
||||||
<-stdoutDone
|
<-stdoutDone
|
||||||
<-stderrDone
|
<-stderrDone
|
||||||
|
_ = stdout.Close()
|
||||||
|
_ = stderr.Close()
|
||||||
waitErrorCh <- err
|
waitErrorCh <- err
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
<-stdoutReady
|
||||||
|
<-stderrReady
|
||||||
|
|
||||||
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamToChannel(ready chan<- struct{},
|
func streamToChannel(ready chan<- struct{}, done chan<- struct{},
|
||||||
stop <-chan struct{}, done chan<- struct{},
|
|
||||||
stream io.Reader, lines chan<- string,
|
stream io.Reader, lines chan<- string,
|
||||||
) {
|
) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
@@ -89,12 +87,5 @@ func streamToChannel(ready chan<- struct{},
|
|||||||
if err == nil || errors.Is(err, os.ErrClosed) {
|
if err == nil || errors.Is(err, os.ErrClosed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
lines <- "stream error: " + err.Error()
|
||||||
// ignore the error if it is stopped.
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
lines <- "stream error: " + err.Error()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,3 +24,28 @@ func portsToString(ports []uint16) (s string) {
|
|||||||
" and " + portStrings[len(portStrings)-1]
|
" and " + portStrings[len(portStrings)-1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func portPairsToString(internalToExternalPort map[uint16]uint16) (s string) {
|
||||||
|
switch len(internalToExternalPort) {
|
||||||
|
case 0:
|
||||||
|
return "no port forwarded"
|
||||||
|
case 1:
|
||||||
|
internal := slices.Collect(maps.Keys(internalToExternalPort))[0]
|
||||||
|
return "port forwarded is " + portPairToString(internal, internalToExternalPort[internal])
|
||||||
|
default:
|
||||||
|
portStrings := make([]string, 0, len(internalToExternalPort))
|
||||||
|
for internal, external := range internalToExternalPort {
|
||||||
|
portStrings = append(portStrings, portPairToString(internal, external))
|
||||||
|
}
|
||||||
|
sort.StringSlice(portStrings).Sort()
|
||||||
|
return "ports forwarded are " + strings.Join(portStrings[:len(portStrings)-1], ", ") +
|
||||||
|
" and " + portStrings[len(portStrings)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func portPairToString(internal, external uint16) string {
|
||||||
|
if internal == external {
|
||||||
|
return strconv.FormatUint(uint64(external), 10)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d (internal port %d)", external, internal)
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,3 +40,42 @@ func Test_portsToString(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_externalInternalPortsToString(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
internalToExternalPort map[uint16]uint16
|
||||||
|
s string
|
||||||
|
}{
|
||||||
|
"no_port": {
|
||||||
|
s: "no port forwarded",
|
||||||
|
},
|
||||||
|
"one_port": {
|
||||||
|
internalToExternalPort: map[uint16]uint16{123: 123},
|
||||||
|
s: "port forwarded is 123",
|
||||||
|
},
|
||||||
|
"two_ports": {
|
||||||
|
internalToExternalPort: map[uint16]uint16{123: 123, 456: 456},
|
||||||
|
s: "ports forwarded are 123 and 456",
|
||||||
|
},
|
||||||
|
"two_ports_different_internal_external": {
|
||||||
|
internalToExternalPort: map[uint16]uint16{123: 124, 456: 457},
|
||||||
|
s: "ports forwarded are 124 (internal port 123) and 457 (internal port 456)",
|
||||||
|
},
|
||||||
|
"three_ports": {
|
||||||
|
internalToExternalPort: map[uint16]uint16{123: 123, 456: 456, 789: 789},
|
||||||
|
s: "ports forwarded are 123, 456 and 789",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := portPairsToString(testCase.internalToExternalPort)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.s, s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) onNewPorts(ctx context.Context, internalToExternalPorts map[uint16]uint16) (err error) {
|
func (s *Service) onNewPorts(ctx context.Context, internalToExternalPorts map[uint16]uint16) (err error) {
|
||||||
|
s.logger.Info(portPairsToString(internalToExternalPorts))
|
||||||
|
|
||||||
|
externalPorts := slices.Collect(maps.Values(internalToExternalPorts))
|
||||||
autoRedirectionNeeded := false
|
autoRedirectionNeeded := false
|
||||||
externalToInternalPorts := make(map[uint16]uint16, len(internalToExternalPorts))
|
externalToInternalPorts := make(map[uint16]uint16, len(internalToExternalPorts))
|
||||||
for internal, external := range internalToExternalPorts {
|
for internal, external := range internalToExternalPorts {
|
||||||
@@ -97,12 +100,7 @@ func (s *Service) onNewPorts(ctx context.Context, internalToExternalPorts map[ui
|
|||||||
autoRedirectionNeeded = true
|
autoRedirectionNeeded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalPorts := slices.Collect(maps.Keys(externalToInternalPorts))
|
|
||||||
slices.Sort(externalPorts)
|
slices.Sort(externalPorts)
|
||||||
|
|
||||||
s.logger.Info(portsToString(externalPorts))
|
|
||||||
|
|
||||||
userRedirectionEnabled := !slices.Equal(s.settings.ListeningPorts, []uint16{0})
|
userRedirectionEnabled := !slices.Equal(s.settings.ListeningPorts, []uint16{0})
|
||||||
for i, port := range externalPorts {
|
for i, port := range externalPorts {
|
||||||
internalPort := externalToInternalPorts[port]
|
internalPort := externalToInternalPorts[port]
|
||||||
|
|||||||
@@ -16,22 +16,25 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
srp "github.com/ProtonMail/go-srp"
|
srp "github.com/ProtonMail/go-srp"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// apiClient is a minimal Proton v4 API client which can handle all the
|
// apiClient is a minimal Proton v4 API client which can handle all the
|
||||||
// oddities of Proton's authentication flow they want to keep hidden
|
// oddities of Proton's authentication flow they want to keep hidden
|
||||||
// from the public.
|
// from the public.
|
||||||
type apiClient struct {
|
type apiClient struct {
|
||||||
apiURLBase string
|
apiURLBase string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
appVersion string
|
appVersion string
|
||||||
userAgent string
|
vpnGtkAppVersion string
|
||||||
generator *rand.ChaCha8
|
userAgent string
|
||||||
|
generator *rand.ChaCha8
|
||||||
|
warner common.Warner
|
||||||
}
|
}
|
||||||
|
|
||||||
// newAPIClient returns an [apiClient] with sane defaults matching Proton's
|
// newAPIClient returns an [apiClient] with sane defaults matching Proton's
|
||||||
// insane expectations.
|
// insane expectations.
|
||||||
func newAPIClient(ctx context.Context, httpClient *http.Client) (client *apiClient, err error) {
|
func newAPIClient(ctx context.Context, httpClient *http.Client, warner common.Warner) (client *apiClient, err error) {
|
||||||
var seed [32]byte
|
var seed [32]byte
|
||||||
_, _ = crand.Read(seed[:])
|
_, _ = crand.Read(seed[:])
|
||||||
generator := rand.NewChaCha8(seed)
|
generator := rand.NewChaCha8(seed)
|
||||||
@@ -46,17 +49,23 @@ func newAPIClient(ctx context.Context, httpClient *http.Client) (client *apiClie
|
|||||||
}
|
}
|
||||||
userAgent := userAgents[generator.Uint64()%uint64(len(userAgents))]
|
userAgent := userAgents[generator.Uint64()%uint64(len(userAgents))]
|
||||||
|
|
||||||
appVersion, err := getMostRecentStableTag(ctx, httpClient)
|
appVersion, err := getMostRecentStableWebAccountTag(ctx, httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting most recent version for proton app: %w", err)
|
return nil, fmt.Errorf("getting most recent version for web-account: %w", err)
|
||||||
|
}
|
||||||
|
vpnGtkAppVersion, err := getMostRecentStableVPNGtkAppTag(ctx, httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting most recent version for linux VPN GTK app: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &apiClient{
|
return &apiClient{
|
||||||
apiURLBase: "https://account.proton.me/api",
|
apiURLBase: "https://account.proton.me/api",
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
appVersion: appVersion,
|
appVersion: appVersion,
|
||||||
userAgent: userAgent,
|
vpnGtkAppVersion: vpnGtkAppVersion,
|
||||||
generator: generator,
|
userAgent: userAgent,
|
||||||
|
generator: generator,
|
||||||
|
warner: warner,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +73,10 @@ func newAPIClient(ctx context.Context, httpClient *http.Client) (client *apiClie
|
|||||||
// to succeed without being blocked by their "security" measures.
|
// to succeed without being blocked by their "security" measures.
|
||||||
// See for example [getMostRecentStableTag] on how the app version must
|
// See for example [getMostRecentStableTag] on how the app version must
|
||||||
// be set to a recent version or they block your request. "SeCuRiTy"...
|
// be set to a recent version or they block your request. "SeCuRiTy"...
|
||||||
func (c *apiClient) setHeaders(request *http.Request, cookie cookie) {
|
func (c *apiClient) setHeaders(request *http.Request, cookie cookie, appVersion string) {
|
||||||
request.Header.Set("Cookie", cookie.String())
|
request.Header.Set("Cookie", cookie.String())
|
||||||
request.Header.Set("User-Agent", c.userAgent)
|
request.Header.Set("User-Agent", c.userAgent)
|
||||||
request.Header.Set("x-pm-appversion", c.appVersion)
|
request.Header.Set("x-pm-appversion", appVersion)
|
||||||
request.Header.Set("x-pm-locale", "en_US")
|
request.Header.Set("x-pm-locale", "en_US")
|
||||||
request.Header.Set("x-pm-uid", cookie.uid)
|
request.Header.Set("x-pm-uid", cookie.uid)
|
||||||
}
|
}
|
||||||
@@ -98,7 +107,11 @@ func (c *apiClient) authenticate(ctx context.Context, email, password string,
|
|||||||
}
|
}
|
||||||
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64,
|
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64,
|
||||||
srpSessionHex, version, err := c.authInfo(ctx, email, unauthCookie)
|
srpSessionHex, version, err := c.authInfo(ctx, email, unauthCookie)
|
||||||
if err != nil {
|
switch {
|
||||||
|
case errors.Is(err, errUsernameEmpty):
|
||||||
|
c.warner.Warn("Username is empty in auth info response, trying with email address instead")
|
||||||
|
username = email
|
||||||
|
case err != nil:
|
||||||
return cookie{}, fmt.Errorf("getting auth information: %w", err)
|
return cookie{}, fmt.Errorf("getting auth information: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +172,7 @@ func (c *apiClient) getUnauthSession(ctx context.Context, sessionID string) (
|
|||||||
unauthCookie := cookie{
|
unauthCookie := cookie{
|
||||||
sessionID: sessionID,
|
sessionID: sessionID,
|
||||||
}
|
}
|
||||||
c.setHeaders(request, unauthCookie)
|
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||||
|
|
||||||
response, err := c.httpClient.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -244,7 +257,7 @@ func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, acces
|
|||||||
uid: uid,
|
uid: uid,
|
||||||
sessionID: sessionID,
|
sessionID: sessionID,
|
||||||
}
|
}
|
||||||
c.setHeaders(request, unauthCookie)
|
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||||
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
||||||
|
|
||||||
response, err := c.httpClient.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
@@ -291,6 +304,8 @@ func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, acces
|
|||||||
return "", errors.New("auth cookie not found")
|
return "", errors.New("auth cookie not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errUsernameEmpty = errors.New("username is empty in response")
|
||||||
|
|
||||||
// authInfo fetches SRP parameters for the account.
|
// authInfo fetches SRP parameters for the account.
|
||||||
func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie cookie) (
|
func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie cookie) (
|
||||||
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64, srpSessionHex string,
|
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64, srpSessionHex string,
|
||||||
@@ -315,7 +330,7 @@ func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie coo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err)
|
return "", "", "", "", "", 0, fmt.Errorf("creating request: %w", err)
|
||||||
}
|
}
|
||||||
c.setHeaders(request, unauthCookie)
|
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||||
request.Header.Set("Content-Type", "application/json")
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
response, err := c.httpClient.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
@@ -358,15 +373,17 @@ func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie coo
|
|||||||
return "", "", "", "", "", 0, errors.New("salt is empty in response")
|
return "", "", "", "", "", 0, errors.New("salt is empty in response")
|
||||||
case info.SRPSession == "":
|
case info.SRPSession == "":
|
||||||
return "", "", "", "", "", 0, errors.New("SRP session is empty in response")
|
return "", "", "", "", "", 0, errors.New("SRP session is empty in response")
|
||||||
case info.Username == "":
|
|
||||||
return "", "", "", "", "", 0, errors.New("username is empty in response")
|
|
||||||
case info.Version == nil:
|
case info.Version == nil:
|
||||||
return "", "", "", "", "", 0, errors.New("version is missing in response")
|
return "", "", "", "", "", 0, errors.New("version is missing in response")
|
||||||
|
case info.Username == "":
|
||||||
|
// Return a sentinel error the caller can handle to try with the email address instead of the username.
|
||||||
|
// Some accounts seem to have no username.
|
||||||
|
err = fmt.Errorf("%w", errUsernameEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
version = int(*info.Version) //nolint:gosec
|
version = int(*info.Version) //nolint:gosec
|
||||||
return info.Username, info.Modulus, info.ServerEphemeral, info.Salt,
|
return info.Username, info.Modulus, info.ServerEphemeral, info.Salt,
|
||||||
info.SRPSession, version, nil
|
info.SRPSession, version, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type cookie struct {
|
type cookie struct {
|
||||||
@@ -422,7 +439,7 @@ func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return cookie{}, fmt.Errorf("creating request: %w", err)
|
return cookie{}, fmt.Errorf("creating request: %w", err)
|
||||||
}
|
}
|
||||||
c.setHeaders(request, unauthCookie)
|
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||||
request.Header.Set("Content-Type", "application/json")
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
response, err := c.httpClient.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
@@ -573,7 +590,9 @@ func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
c.setHeaders(request, cookie)
|
// Note we use the vpnGtkAppVersion field given it produces an output of more servers
|
||||||
|
c.setHeaders(request, cookie, c.vpnGtkAppVersion)
|
||||||
|
request.Header.Set("x-pm-appversion", "linux-vpn@4.15.2")
|
||||||
|
|
||||||
response, err := c.httpClient.Do(request)
|
response, err := c.httpClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
|||||||
return nil, fmt.Errorf("%w: password is empty", common.ErrCredentialsMissing)
|
return nil, fmt.Errorf("%w: password is empty", common.ErrCredentialsMissing)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiClient, err := newAPIClient(ctx, u.client)
|
apiClient, err := newAPIClient(ctx, u.client, u.warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating API client: %w", err)
|
return nil, fmt.Errorf("creating API client: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getMostRecentStableTag finds the most recent proton-account stable tag version,
|
// getMostRecentStableWebAccountTag finds the most recent proton-account stable tag version,
|
||||||
// in order to use it in the x-pm-appversion http request header. Because if we do
|
// in order to use it in the x-pm-appversion http request header. Because if we do
|
||||||
// fall behind on versioning, Proton doesn't like it because they like to create
|
// fall behind on versioning, Proton doesn't like it because they like to create
|
||||||
// complications where there is no need for it. Hence this function.
|
// complications where there is no need for it. Hence this function.
|
||||||
func getMostRecentStableTag(ctx context.Context, client *http.Client) (version string, err error) {
|
func getMostRecentStableWebAccountTag(ctx context.Context, client *http.Client) (version string, err error) {
|
||||||
page := 1
|
page := 1
|
||||||
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
|
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
|
||||||
for ctx.Err() == nil {
|
for ctx.Err() == nil {
|
||||||
@@ -69,3 +72,45 @@ func getMostRecentStableTag(ctx context.Context, client *http.Client) (version s
|
|||||||
|
|
||||||
return "", fmt.Errorf("%w (queried %d pages)", context.Canceled, page)
|
return "", fmt.Errorf("%w (queried %d pages)", context.Canceled, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getMostRecentStableVPNGtkAppTag finds the latest proton-vpn-gtk-app semver tag,
|
||||||
|
// in order to use it in the x-pm-appversion http request header ONLY to fetch servers
|
||||||
|
// data. Because if we do fall behind on versioning, Proton doesn't like it because they like
|
||||||
|
// to create complications where there is no need for it. Hence this function.
|
||||||
|
func getMostRecentStableVPNGtkAppTag(ctx context.Context, client *http.Client) (version string, err error) {
|
||||||
|
const url = "https://api.github.com/repos/ProtonVPN/proton-vpn-gtk-app/tags?per_page=30"
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||||
|
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("HTTP status code not OK: %s", response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(response.Body)
|
||||||
|
var data []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
err = decoder.Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decoding JSON response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort tags by semver. Invalid tags are placed at the end and we ignore them.
|
||||||
|
// Yes, proton does push invalid semver tag names sometimes. Good job yet again.
|
||||||
|
sort.Slice(data, func(i, j int) bool {
|
||||||
|
return semver.Compare(data[i].Name, data[j].Name) > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
version = "linux-vpn@" + data[0].Name[1:] // remove leading v
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user