chore: do not use sentinel errors when unneeded

- main reason being it's a burden to always define sentinel errors at global scope, wrap them with `%w` instead of using a string directly
- only use sentinel errors when it has to be checked using `errors.Is`
- replace all usage of these sentinel errors in `fmt.Errorf` with direct strings that were in the sentinel error
- exclude the sentinel error definition requirement from .golangci.yml
- update unit tests to use ContainersError instead of ErrorIs so it stays as a "not a change detector test" without requiring a sentinel error
This commit is contained in:
Quentin McGaw
2026-05-02 00:50:16 +00:00
parent 9b6f048fe8
commit 4a78989d9d
172 changed files with 666 additions and 1433 deletions
+4 -12
View File
@@ -12,8 +12,6 @@ import (
"github.com/qdm12/gluetun/internal/provider/utils"
)
var ErrServerPortForwardNotSupported = errors.New("server does not support port forwarding")
const nonSymmetricPortStart uint16 = 56789
// PortForward obtains a VPN server side port forwarded from ProtonVPN gateway.
@@ -21,7 +19,7 @@ func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObj
internalToExternalPorts map[uint16]uint16, err error,
) {
if !objects.CanPortForward {
return nil, fmt.Errorf("%w", ErrServerPortForwardNotSupported)
return nil, errors.New("server does not support port forwarding")
}
client := natpmp.New()
@@ -104,11 +102,6 @@ func checkExternalPorts(logger utils.Logger, udpPort, tcpPort uint16) {
}
}
var (
ErrInternalPortChanged = errors.New("internal port changed")
ErrExternalPortChanged = errors.New("external port changed")
)
func (p *Provider) KeepPortForward(ctx context.Context,
objects utils.PortForwardObjects,
) (err error) {
@@ -135,11 +128,10 @@ func (p *Provider) KeepPortForward(ctx context.Context,
}
checkLifetime(logger, networkProtocol, lifetime, assignedLiftetime)
if externalPort != assignedExternalPort {
return fmt.Errorf("%w: %d changed to %d",
ErrExternalPortChanged, externalPort, assignedExternalPort)
return fmt.Errorf("external port changed from %d to %d", externalPort, assignedExternalPort)
} else if internalPort != assignedInternalPort {
return fmt.Errorf("%w: %d (for external port %d) changed to %d",
ErrInternalPortChanged, internalPort, externalPort, assignedInternalPort)
return fmt.Errorf("internal port changed from %d (for external port %d) to %d",
internalPort, externalPort, assignedInternalPort)
}
}
objects.Logger.Debug(fmt.Sprintf("port forwarded %d maintained", externalPort))
+34 -53
View File
@@ -60,8 +60,6 @@ func newAPIClient(ctx context.Context, httpClient *http.Client) (client *apiClie
}, nil
}
var ErrCodeNotSuccess = errors.New("response code is not success")
// setHeaders sets the minimal necessary headers for Proton API requests
// to succeed without being blocked by their "security" measures.
// See for example [getMostRecentStableTag] on how the app version must
@@ -126,8 +124,6 @@ func (c *apiClient) authenticate(ctx context.Context, email, password string,
return authCookie, nil
}
var ErrSessionIDNotFound = errors.New("session ID not found in cookies")
func (c *apiClient) getSessionID(ctx context.Context) (sessionID string, err error) {
const url = "https://account.proton.me/vpn"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
@@ -150,11 +146,9 @@ func (c *apiClient) getSessionID(ctx context.Context) (sessionID string, err err
}
}
return "", fmt.Errorf("%w", ErrSessionIDNotFound)
return "", errors.New("session ID not found in cookies")
}
var ErrDataFieldMissing = errors.New("data field missing in response")
func (c *apiClient) getUnauthSession(ctx context.Context, sessionID string) (
tokenType, accessToken, refreshToken, uid string, err error,
) {
@@ -198,24 +192,22 @@ func (c *apiClient) getUnauthSession(ctx context.Context, sessionID string) (
const successCode = 1000
switch {
case data.Code != successCode:
return "", "", "", "", fmt.Errorf("%w: expected %d got %d",
ErrCodeNotSuccess, successCode, data.Code)
return "", "", "", "", fmt.Errorf("response code %d is not expected success code %d",
data.Code, successCode)
case data.AccessToken == "":
return "", "", "", "", fmt.Errorf("%w: access token is empty", ErrDataFieldMissing)
return "", "", "", "", errors.New("access token is empty in response")
case data.RefreshToken == "":
return "", "", "", "", fmt.Errorf("%w: refresh token is empty", ErrDataFieldMissing)
return "", "", "", "", errors.New("refresh token is empty in response")
case data.TokenType == "":
return "", "", "", "", fmt.Errorf("%w: token type is empty", ErrDataFieldMissing)
return "", "", "", "", errors.New("token type is empty in response")
case data.UID == "":
return "", "", "", "", fmt.Errorf("%w: UID is empty", ErrDataFieldMissing)
return "", "", "", "", errors.New("UID is empty in response")
}
// Ignore Scopes and LocalID fields, we don't use them.
return data.TokenType, data.AccessToken, data.RefreshToken, data.UID, nil
}
var ErrUIDMismatch = errors.New("UID in response does not match request UID")
func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, accessToken,
refreshToken, uid string,
) (cookieToken string, err error) {
@@ -282,11 +274,11 @@ func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, acces
const successCode = 1000
switch {
case cookies.Code != successCode:
return "", fmt.Errorf("%w: expected %d got %d",
ErrCodeNotSuccess, successCode, cookies.Code)
return "", fmt.Errorf("response code %d is not expected success code %d",
cookies.Code, successCode)
case cookies.UID != requestBody.UID:
return "", fmt.Errorf("%w: expected %s got %s",
ErrUIDMismatch, requestBody.UID, cookies.UID)
return "", fmt.Errorf("UID %s in response does not match request UID %s",
cookies.UID, requestBody.UID)
}
// Ignore LocalID and RefreshCounter fields, we don't use them.
@@ -296,11 +288,9 @@ func (c *apiClient) cookieToken(ctx context.Context, sessionID, tokenType, acces
}
}
return "", fmt.Errorf("%w", ErrAuthCookieNotFound)
return "", errors.New("auth cookie not found")
}
var ErrUsernameDoesNotExist = errors.New("username does not exist")
// authInfo fetches SRP parameters for the account.
func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie cookie) (
username, modulusPGPClearSigned, serverEphemeralBase64, saltBase64, srpSessionHex string,
@@ -358,20 +348,20 @@ func (c *apiClient) authInfo(ctx context.Context, email string, unauthCookie coo
const successCode = 1000
switch {
case info.Code != successCode:
return "", "", "", "", "", 0, fmt.Errorf("%w: expected %d got %d",
ErrCodeNotSuccess, successCode, info.Code)
return "", "", "", "", "", 0, fmt.Errorf("response code %d is not expected success code %d",
info.Code, successCode)
case info.Modulus == "":
return "", "", "", "", "", 0, fmt.Errorf("%w: modulus is empty", ErrDataFieldMissing)
return "", "", "", "", "", 0, errors.New("modulus is empty in response")
case info.ServerEphemeral == "":
return "", "", "", "", "", 0, fmt.Errorf("%w: server ephemeral is empty", ErrDataFieldMissing)
return "", "", "", "", "", 0, errors.New("server ephemeral is empty in response")
case info.Salt == "":
return "", "", "", "", "", 0, fmt.Errorf("%w (salt data field is empty)", ErrUsernameDoesNotExist)
return "", "", "", "", "", 0, errors.New("salt is empty in response")
case info.SRPSession == "":
return "", "", "", "", "", 0, fmt.Errorf("%w: SRP session is empty", ErrDataFieldMissing)
return "", "", "", "", "", 0, errors.New("SRP session is empty in response")
case info.Username == "":
return "", "", "", "", "", 0, fmt.Errorf("%w: username is empty", ErrDataFieldMissing)
return "", "", "", "", "", 0, errors.New("username is empty in response")
case info.Version == nil:
return "", "", "", "", "", 0, fmt.Errorf("%w: version is missing", ErrDataFieldMissing)
return "", "", "", "", "", 0, errors.New("version is missing in response")
}
version = int(*info.Version) //nolint:gosec
@@ -399,13 +389,7 @@ func (c *cookie) String() string {
return s
}
var (
// ErrServerProofNotValid indicates the M2 from the server didn't match the expected proof.
ErrServerProofNotValid = errors.New("server proof from server is not valid")
ErrVPNScopeNotFound = errors.New("VPN scope not found in scopes")
ErrTwoFANotSupported = errors.New("two factor authentication not supported in this client")
ErrAuthCookieNotFound = errors.New("auth cookie not found")
)
// ErrServerProofNotValid indicates the M2 from the server didn't match the expected proof.
// auth performs the SRP proof submission (and optionally TOTP) to obtain tokens.
func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
@@ -495,22 +479,22 @@ func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
return cookie{}, fmt.Errorf("decoding server proof: %w", err)
}
if !bytes.Equal(m2, proofs.ExpectedServerProof) {
return cookie{}, fmt.Errorf("%w: expected %x got %x",
ErrServerProofNotValid, proofs.ExpectedServerProof, m2)
return cookie{}, fmt.Errorf("server proof from server %x is not expected proof %x",
m2, proofs.ExpectedServerProof)
}
const successCode = 1000
switch {
case auth.Code != successCode:
return cookie{}, fmt.Errorf("%w: expected %d got %d",
ErrCodeNotSuccess, successCode, auth.Code)
return cookie{}, fmt.Errorf("response code %d is not expected success code %d",
auth.Code, successCode)
case auth.UID != unauthCookie.uid:
return cookie{}, fmt.Errorf("%w: expected %s got %s",
ErrUIDMismatch, unauthCookie.uid, auth.UID)
return cookie{}, fmt.Errorf("UID %s in response does not match request UID %s",
auth.UID, unauthCookie.uid)
case auth.TwoFactor != 0:
return cookie{}, fmt.Errorf("%w", ErrTwoFANotSupported)
return cookie{}, errors.New("two factor authentication not supported in this client")
case !slices.Contains(auth.Scopes, "vpn"):
return cookie{}, fmt.Errorf("%w: in %v", ErrVPNScopeNotFound, auth.Scopes)
return cookie{}, fmt.Errorf("VPN scope not found in scopes %v", auth.Scopes)
}
for _, setCookieHeader := range response.Header.Values("Set-Cookie") {
@@ -524,8 +508,7 @@ func (c *apiClient) auth(ctx context.Context, unauthCookie cookie,
}
}
return cookie{}, fmt.Errorf("%w: in HTTP headers %s",
ErrAuthCookieNotFound, httpHeadersToString(response.Header))
return cookie{}, fmt.Errorf("auth cookie not found in HTTP headers %s", httpHeadersToString(response.Header))
}
// generateLettersDigits mimicing Proton's own random string generator:
@@ -611,8 +594,6 @@ func (c *apiClient) fetchServers(ctx context.Context, cookie cookie) (
return data, nil
}
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
func buildError(httpCode int, body []byte) error {
prettyCode := http.StatusText(httpCode)
var protonError struct {
@@ -624,8 +605,8 @@ func buildError(httpCode int, body []byte) error {
decoder.DisallowUnknownFields()
err := decoder.Decode(&protonError)
if err != nil || protonError.Error == nil || protonError.Code == nil {
return fmt.Errorf("%w: %s: %s",
ErrHTTPStatusCodeNotOK, prettyCode, body)
return fmt.Errorf("HTTP status code not OK: %s: %s",
prettyCode, body)
}
details := make([]string, 0, len(protonError.Details))
@@ -633,6 +614,6 @@ func buildError(httpCode int, body []byte) error {
details = append(details, fmt.Sprintf("%s: %s", key, value))
}
return fmt.Errorf("%w: %s: %s (code %d with details: %s)",
ErrHTTPStatusCodeNotOK, prettyCode, *protonError.Error, *protonError.Code, strings.Join(details, ", "))
return fmt.Errorf("HTTP status code not OK: %s: %s (code %d with details: %s)",
prettyCode, *protonError.Error, *protonError.Code, strings.Join(details, ", "))
}
@@ -45,7 +45,7 @@ func getMostRecentStableTag(ctx context.Context, client *http.Client) (version s
}
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("%w: %s: %s", ErrHTTPStatusCodeNotOK, response.Status, data)
return "", fmt.Errorf("HTTP status code not OK: %s: %s", response.Status, data)
}
var tags []struct {