catalog.go 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. package pia
  2. import (
  3. "context"
  4. "sync"
  5. "time"
  6. )
  7. type Catalog struct {
  8. Source ServerListSource
  9. CacheTTL time.Duration
  10. Now func() time.Time
  11. mu sync.Mutex
  12. cached []Region
  13. schema string
  14. verified bool
  15. fetchedAt time.Time
  16. refreshing chan struct{}
  17. }
  18. func NewCatalog(source ServerListSource) *Catalog {
  19. return &Catalog{Source: source, CacheTTL: DefaultCatalogFreshTTL, Now: time.Now}
  20. }
  21. func (c *Catalog) ListRegions(ctx context.Context) ([]Region, string, error) {
  22. for {
  23. c.mu.Lock()
  24. age := c.Now().Sub(c.fetchedAt)
  25. if len(c.cached) > 0 && c.verified && c.CacheTTL > 0 && age >= 0 && age < c.CacheTTL {
  26. regions, schema := cloneRegions(c.cached), c.schema
  27. c.mu.Unlock()
  28. return regions, schema, nil
  29. }
  30. if wait := c.refreshing; wait != nil {
  31. c.mu.Unlock()
  32. select {
  33. case <-ctx.Done():
  34. return nil, "", ctx.Err()
  35. case <-wait:
  36. continue
  37. }
  38. }
  39. done := make(chan struct{})
  40. c.refreshing = done
  41. c.mu.Unlock()
  42. return c.fetchAndPublish(ctx, done)
  43. }
  44. }
  45. func (c *Catalog) fetchAndPublish(ctx context.Context, done chan struct{}) ([]Region, string, error) {
  46. defer func() {
  47. c.mu.Lock()
  48. c.refreshing = nil
  49. close(done)
  50. c.mu.Unlock()
  51. }()
  52. snapshot, err := c.Source.Fetch(ctx)
  53. var regions []Region
  54. var schema string
  55. if err == nil && !snapshot.SignatureVerified {
  56. err = NewError(CodeCatalogSignatureInvalid, "The PIA region list was not signature-verified.")
  57. }
  58. if err == nil {
  59. regions, schema, err = ParseServerList(snapshot.Payload, snapshot.SchemaHint)
  60. }
  61. if err != nil {
  62. return nil, "", err
  63. }
  64. c.mu.Lock()
  65. c.cached = cloneRegions(regions)
  66. c.schema = schema
  67. c.verified = true
  68. c.fetchedAt = c.Now()
  69. c.mu.Unlock()
  70. return cloneRegions(regions), schema, nil
  71. }
  72. func cloneRegions(regions []Region) []Region {
  73. result := make([]Region, len(regions))
  74. for i, region := range regions {
  75. result[i] = region
  76. result[i].WireGuard = append([]WireGuardServer(nil), region.WireGuard...)
  77. }
  78. return result
  79. }