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 "" }