mirror of
https://github.com/qdm12/gluetun.git
synced 2026-05-06 20:10:11 +02:00
feat(protonvpn): updater finds more servers using app-version linux-vpn
This commit is contained in:
@@ -26,6 +26,7 @@ require (
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
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/sys v0.42.0
|
||||
golang.org/x/text v0.35.0
|
||||
@@ -57,7 +58,6 @@ require (
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // 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/tools v0.42.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
|
||||
@@ -25,6 +25,7 @@ type apiClient struct {
|
||||
apiURLBase string
|
||||
httpClient *http.Client
|
||||
appVersion string
|
||||
vpnGtkAppVersion string
|
||||
userAgent string
|
||||
generator *rand.ChaCha8
|
||||
}
|
||||
@@ -46,15 +47,20 @@ func newAPIClient(ctx context.Context, httpClient *http.Client) (client *apiClie
|
||||
}
|
||||
userAgent := userAgents[generator.Uint64()%uint64(len(userAgents))]
|
||||
|
||||
appVersion, err := getMostRecentStableTag(ctx, httpClient)
|
||||
appVersion, err := getMostRecentStableWebAccountTag(ctx, httpClient)
|
||||
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{
|
||||
apiURLBase: "https://account.proton.me/api",
|
||||
httpClient: httpClient,
|
||||
appVersion: appVersion,
|
||||
vpnGtkAppVersion: vpnGtkAppVersion,
|
||||
userAgent: userAgent,
|
||||
generator: generator,
|
||||
}, nil
|
||||
@@ -66,10 +72,10 @@ var ErrCodeNotSuccess = errors.New("response code is not success")
|
||||
// to succeed without being blocked by their "security" measures.
|
||||
// See for example [getMostRecentStableTag] on how the app version must
|
||||
// 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("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-uid", cookie.uid)
|
||||
}
|
||||
@@ -165,7 +171,7 @@ func (c *apiClient) getUnauthSession(ctx context.Context, sessionID string) (
|
||||
unauthCookie := cookie{
|
||||
sessionID: sessionID,
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
@@ -252,7 +258,7 @@ func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, acces
|
||||
uid: uid,
|
||||
sessionID: sessionID,
|
||||
}
|
||||
c.setHeaders(request, unauthCookie)
|
||||
c.setHeaders(request, unauthCookie, c.appVersion)
|
||||
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
@@ -325,7 +331,7 @@ func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie coo
|
||||
if err != nil {
|
||||
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")
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
@@ -438,7 +444,7 @@ func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
|
||||
if err != nil {
|
||||
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")
|
||||
|
||||
response, err := c.httpClient.Do(request)
|
||||
@@ -590,7 +596,9 @@ func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,15 +7,18 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"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
|
||||
// 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 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
|
||||
regexVersion := regexp.MustCompile(`^proton-account@(\d+\.\d+\.\d+\.\d+)$`)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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("%w: %s", ErrHTTPStatusCodeNotOK, 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