1
0

serverlist_client.go 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. package pia
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "path"
  8. "strings"
  9. )
  10. type ServerListSource interface {
  11. Fetch(ctx context.Context) (ServerListSnapshot, error)
  12. }
  13. type ServerListSnapshot struct {
  14. Payload []byte
  15. SchemaHint string
  16. SignatureVerified bool
  17. }
  18. type CatalogClient struct {
  19. Endpoint string
  20. PublicKeyPEM []byte
  21. HTTPClient *http.Client
  22. MaxBody int64
  23. UserAgent string
  24. }
  25. func NewCatalogClient(endpoint string, publicKey []byte) *CatalogClient {
  26. return &CatalogClient{
  27. Endpoint: endpoint,
  28. PublicKeyPEM: publicKey,
  29. MaxBody: DefaultMaxServerListBody,
  30. UserAgent: DefaultUserAgent,
  31. HTTPClient: &http.Client{Timeout: DefaultRequestTimeout, CheckRedirect: noRedirect},
  32. }
  33. }
  34. func (c *CatalogClient) Fetch(ctx context.Context) (ServerListSnapshot, error) {
  35. request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.Endpoint, nil)
  36. if err != nil {
  37. return ServerListSnapshot{}, WrapError(CodeCatalogUnavailable, "The PIA region-list endpoint is invalid.", err)
  38. }
  39. request.Header.Set("Accept", "application/json, text/plain;q=0.9")
  40. request.Header.Set("User-Agent", c.UserAgent)
  41. response, err := c.HTTPClient.Do(request)
  42. if err != nil {
  43. return ServerListSnapshot{}, classifyNetworkError(ctx, CodeCatalogUnavailable, "The PIA region list could not be downloaded.", err)
  44. }
  45. defer response.Body.Close()
  46. if response.StatusCode != http.StatusOK {
  47. return ServerListSnapshot{}, NewError(CodeCatalogUnavailable, fmt.Sprintf("PIA returned HTTP %d for the region list.", response.StatusCode))
  48. }
  49. if !expectedContentType(response.Header.Get("Content-Type"), "application/json", "text/plain", "application/octet-stream") {
  50. return ServerListSnapshot{}, NewError(CodeCatalogSchemaUnsupported, "PIA returned an unexpected region-list content type.")
  51. }
  52. raw, err := readLimitedBody(response.Body, c.MaxBody)
  53. if err != nil {
  54. return ServerListSnapshot{}, WrapError(CodeCatalogUnavailable, "The PIA region-list response is too large or incomplete.", err)
  55. }
  56. verified, err := VerifySignedServerList(raw, c.PublicKeyPEM)
  57. if err != nil {
  58. return ServerListSnapshot{}, err
  59. }
  60. return ServerListSnapshot{Payload: verified, SchemaHint: schemaHint(c.Endpoint), SignatureVerified: true}, nil
  61. }
  62. func schemaHint(endpoint string) string {
  63. parsed, err := url.Parse(endpoint)
  64. if err != nil {
  65. return ""
  66. }
  67. base := strings.ToLower(path.Base(parsed.Path))
  68. if base == "v6" || base == "v7" {
  69. return strings.TrimPrefix(base, "v")
  70. }
  71. return ""
  72. }