mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-24 20:07:13 +00:00
bd6a6aba43
* 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.
85 lines
3.1 KiB
Go
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
|
|
}
|