Files
3x-ui-agent/internal/pia/auth.go
T
Sanaei f727d04f65
Release 3X-UI for Docker / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
v3.7.0
2026-08-24 15:07:15 +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
}