Files
3x-ui/internal/pia/serverlist_client.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

80 lines
2.5 KiB
Go

package pia
import (
"context"
"fmt"
"net/http"
"net/url"
"path"
"strings"
)
type ServerListSource interface {
Fetch(ctx context.Context) (ServerListSnapshot, error)
}
type ServerListSnapshot struct {
Payload []byte
SchemaHint string
SignatureVerified bool
}
type CatalogClient struct {
Endpoint string
PublicKeyPEM []byte
HTTPClient *http.Client
MaxBody int64
UserAgent string
}
func NewCatalogClient(endpoint string, publicKey []byte) *CatalogClient {
return &CatalogClient{
Endpoint: endpoint,
PublicKeyPEM: publicKey,
MaxBody: DefaultMaxServerListBody,
UserAgent: DefaultUserAgent,
HTTPClient: &http.Client{Timeout: DefaultRequestTimeout, CheckRedirect: noRedirect},
}
}
func (c *CatalogClient) Fetch(ctx context.Context) (ServerListSnapshot, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.Endpoint, nil)
if err != nil {
return ServerListSnapshot{}, WrapError(CodeCatalogUnavailable, "The PIA region-list endpoint is invalid.", err)
}
request.Header.Set("Accept", "application/json, text/plain;q=0.9")
request.Header.Set("User-Agent", c.UserAgent)
response, err := c.HTTPClient.Do(request)
if err != nil {
return ServerListSnapshot{}, classifyNetworkError(ctx, CodeCatalogUnavailable, "The PIA region list could not be downloaded.", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return ServerListSnapshot{}, NewError(CodeCatalogUnavailable, fmt.Sprintf("PIA returned HTTP %d for the region list.", response.StatusCode))
}
if !expectedContentType(response.Header.Get("Content-Type"), "application/json", "text/plain", "application/octet-stream") {
return ServerListSnapshot{}, NewError(CodeCatalogSchemaUnsupported, "PIA returned an unexpected region-list content type.")
}
raw, err := readLimitedBody(response.Body, c.MaxBody)
if err != nil {
return ServerListSnapshot{}, WrapError(CodeCatalogUnavailable, "The PIA region-list response is too large or incomplete.", err)
}
verified, err := VerifySignedServerList(raw, c.PublicKeyPEM)
if err != nil {
return ServerListSnapshot{}, err
}
return ServerListSnapshot{Payload: verified, SchemaHint: schemaHint(c.Endpoint), SignatureVerified: true}, nil
}
func schemaHint(endpoint string) string {
parsed, err := url.Parse(endpoint)
if err != nil {
return ""
}
base := strings.ToLower(path.Base(parsed.Path))
if base == "v6" || base == "v7" {
return strings.TrimPrefix(base, "v")
}
return ""
}