Files
3x-ui/internal/pia/auth.go
T
Masterain bd6a6aba43 feat(pia): add PIA login-and-add WireGuard outbounds (#6272)
* feat(pia): add login-and-add WireGuard outbounds (#2)

* fix(pia): keep PIA outbounds identifiable after the editor strips hostname

The outbound editor drops piaHostname, so last-segment matching failed for hyphenated servers. Identify rows by the computed tag, re-encrypt stored tokens onto the active key, skip unusable catalog rows, and always release the catalog refresh latch.
2026-08-22 23:11:06 +02:00

85 lines
3.1 KiB
Go

package pia
import (
"bytes"
"context"
"fmt"
"mime/multipart"
"net/http"
"strings"
"time"
)
type AuthClient struct {
Endpoint string
HTTPClient *http.Client
MaxBody int64
UserAgent string
Now func() time.Time
}
func NewAuthClient(endpoint string) *AuthClient {
return &AuthClient{
Endpoint: endpoint,
MaxBody: DefaultMaxResponseBody,
UserAgent: DefaultUserAgent,
Now: time.Now,
HTTPClient: &http.Client{Timeout: DefaultRequestTimeout, CheckRedirect: noRedirect},
}
}
func (c *AuthClient) Authenticate(ctx context.Context, username string, password []byte) (Token, error) {
username = strings.TrimSpace(username)
if !validSecret([]byte(username), 1, 256) || !validSecret(password, 1, 1024) {
return Token{}, NewError(CodeInvalidCredentials, "Enter a valid PIA username and password.")
}
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("username", username); err != nil {
return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
}
if err := writer.WriteField("password", string(password)); err != nil {
return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
}
if err := writer.Close(); err != nil {
return Token{}, WrapError(CodeAuthenticationUnavailable, "Could not prepare the authentication request.", err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Endpoint, &body)
if err != nil {
return Token{}, WrapError(CodeAuthenticationUnavailable, "The authentication endpoint is invalid.", err)
}
request.Header.Set("Content-Type", writer.FormDataContentType())
request.Header.Set("Accept", "application/json")
request.Header.Set("User-Agent", c.UserAgent)
response, err := c.HTTPClient.Do(request)
if err != nil {
return Token{}, classifyNetworkError(ctx, CodeAuthenticationUnavailable, "PIA authentication could not be reached.", err)
}
defer response.Body.Close()
if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
return Token{}, NewError(CodeInvalidCredentials, "The PIA username or password was rejected.")
}
if response.StatusCode != http.StatusOK {
return Token{}, NewError(CodeAuthenticationUnavailable, fmt.Sprintf("PIA authentication returned HTTP %d.", response.StatusCode))
}
if !expectedContentType(response.Header.Get("Content-Type"), "application/json") {
return Token{}, NewError(CodeAuthenticationUnavailable, "PIA authentication returned an unexpected content type.")
}
raw, err := readLimitedBody(response.Body, c.MaxBody)
if err != nil {
return Token{}, WrapError(CodeAuthenticationUnavailable, "PIA authentication returned an invalid response.", err)
}
var payload struct {
Token string `json:"token"`
}
if err := decodeSingleJSON(raw, &payload); err != nil || !validSecret([]byte(payload.Token), 16, 4096) {
return Token{}, NewError(CodeAuthenticationUnavailable, "PIA authentication returned an invalid token response.")
}
now := time.Now
if c.Now != nil {
now = c.Now
}
return Token{Value: []byte(payload.Token), ExpiresAt: now().Add(DefaultTokenTTL)}, nil
}