1
0

serverlist_parser.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. package pia
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/netip"
  6. "regexp"
  7. "sort"
  8. "strings"
  9. )
  10. var (
  11. regionIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`)
  12. countryCodePattern = regexp.MustCompile(`^[A-Za-z]{2}$`)
  13. )
  14. type ServerListParser interface {
  15. Schema() string
  16. CanParse(raw []byte) bool
  17. Parse(raw []byte) ([]Region, error)
  18. }
  19. type (
  20. V6Parser struct{}
  21. V7Parser struct{}
  22. )
  23. func (V6Parser) Schema() string { return "v6" }
  24. func (V7Parser) Schema() string { return "v7" }
  25. func (V6Parser) CanParse(raw []byte) bool { return schemaVersion(raw) == 0 || schemaVersion(raw) == 6 }
  26. func (V7Parser) CanParse(raw []byte) bool { return schemaVersion(raw) == 7 }
  27. func (V6Parser) Parse(raw []byte) ([]Region, error) { return parseCatalog(raw, false) }
  28. func (V7Parser) Parse(raw []byte) ([]Region, error) { return parseCatalog(raw, true) }
  29. type catalogEnvelope struct {
  30. Version json.RawMessage `json:"version"`
  31. Groups map[string]json.RawMessage `json:"groups"`
  32. Regions []rawRegion `json:"regions"`
  33. }
  34. type rawRegion struct {
  35. ID string `json:"id"`
  36. Name string `json:"name"`
  37. Country string `json:"country"`
  38. Geo *bool `json:"geo"`
  39. Offline *bool `json:"offline"`
  40. PortForward *bool `json:"port_forward"`
  41. PortForwarding *bool `json:"port_forwarding"`
  42. Servers rawServers `json:"servers"`
  43. }
  44. type rawServers struct {
  45. WireGuard []rawServer `json:"wg"`
  46. }
  47. type rawServer struct {
  48. IP string `json:"ip"`
  49. CN string `json:"cn"`
  50. Hostname string `json:"hostname"`
  51. }
  52. func ParseServerList(raw []byte, schemaHint string) ([]Region, string, error) {
  53. parsers := []ServerListParser{V7Parser{}, V6Parser{}}
  54. version, present, err := detectSchemaVersion(raw)
  55. if err != nil {
  56. return nil, "", WrapError(CodeCatalogSchemaUnsupported, "PIA returned an invalid server-list version.", err)
  57. }
  58. if present {
  59. for _, parser := range parsers {
  60. if strings.TrimPrefix(parser.Schema(), "v") == fmt.Sprint(version) {
  61. regions, parseErr := parser.Parse(raw)
  62. return regions, parser.Schema(), parseErr
  63. }
  64. }
  65. return nil, "", NewError(CodeCatalogSchemaUnsupported, "This PIA server-list schema is not supported.")
  66. }
  67. hint := strings.ToLower(strings.TrimPrefix(schemaHint, "v"))
  68. if hint != "" {
  69. for _, parser := range parsers {
  70. if strings.TrimPrefix(parser.Schema(), "v") != hint {
  71. continue
  72. }
  73. regions, err := parser.Parse(raw)
  74. return regions, parser.Schema(), err
  75. }
  76. }
  77. for _, parser := range parsers {
  78. if parser.CanParse(raw) {
  79. regions, err := parser.Parse(raw)
  80. return regions, parser.Schema(), err
  81. }
  82. }
  83. return nil, "", NewError(CodeCatalogSchemaUnsupported, "This PIA server-list schema is not supported.")
  84. }
  85. func schemaVersion(raw []byte) int {
  86. version, present, err := detectSchemaVersion(raw)
  87. if err != nil || !present {
  88. return 0
  89. }
  90. return version
  91. }
  92. func detectSchemaVersion(raw []byte) (int, bool, error) {
  93. var envelope struct {
  94. Version json.RawMessage `json:"version"`
  95. }
  96. if err := json.Unmarshal(raw, &envelope); err != nil {
  97. return 0, false, err
  98. }
  99. if len(envelope.Version) == 0 || string(envelope.Version) == "null" {
  100. return 0, false, nil
  101. }
  102. var number int
  103. if json.Unmarshal(envelope.Version, &number) == nil {
  104. if number < 1 {
  105. return 0, true, fmt.Errorf("version must be positive")
  106. }
  107. return number, true, nil
  108. }
  109. var text string
  110. if json.Unmarshal(envelope.Version, &text) == nil {
  111. text = strings.TrimPrefix(strings.ToLower(text), "v")
  112. if _, err := fmt.Sscanf(text, "%d", &number); err == nil && fmt.Sprint(number) == text && number > 0 {
  113. return number, true, nil
  114. }
  115. }
  116. return 0, true, fmt.Errorf("version has an unsupported type or value")
  117. }
  118. func parseCatalog(raw []byte, allowV7Aliases bool) ([]Region, error) {
  119. var envelope catalogEnvelope
  120. if err := decodeSingleJSON(raw, &envelope); err != nil {
  121. return nil, WrapError(CodeCatalogSchemaUnsupported, "PIA returned an invalid region list.", err)
  122. }
  123. if len(envelope.Groups) == 0 || len(envelope.Regions) == 0 {
  124. return nil, NewError(CodeCatalogSchemaUnsupported, "The PIA region list is missing required fields.")
  125. }
  126. seen := make(map[string]struct{}, len(envelope.Regions))
  127. regions := make([]Region, 0, len(envelope.Regions))
  128. for _, rawRegion := range envelope.Regions {
  129. if !regionIDPattern.MatchString(rawRegion.ID) || strings.TrimSpace(rawRegion.Name) == "" || len(rawRegion.Name) > 128 {
  130. continue
  131. }
  132. idKey := strings.ToLower(rawRegion.ID)
  133. if _, duplicate := seen[idKey]; duplicate {
  134. continue
  135. }
  136. seen[idKey] = struct{}{}
  137. if !countryCodePattern.MatchString(rawRegion.Country) || rawRegion.Geo == nil || rawRegion.Offline == nil {
  138. continue
  139. }
  140. if *rawRegion.Offline {
  141. continue
  142. }
  143. portForwarding := false
  144. if rawRegion.PortForward != nil {
  145. portForwarding = *rawRegion.PortForward
  146. } else if allowV7Aliases && rawRegion.PortForwarding != nil {
  147. portForwarding = *rawRegion.PortForwarding
  148. }
  149. servers := make([]WireGuardServer, 0, len(rawRegion.Servers.WireGuard))
  150. for _, rawServer := range rawRegion.Servers.WireGuard {
  151. hostname := rawServer.CN
  152. if hostname == "" && allowV7Aliases {
  153. hostname = rawServer.Hostname
  154. }
  155. ip, err := netip.ParseAddr(rawServer.IP)
  156. if err != nil || !ip.Is4() || ip.IsUnspecified() || !validHostname(hostname) {
  157. continue
  158. }
  159. servers = append(servers, WireGuardServer{Hostname: hostname, IP: ip})
  160. }
  161. if len(servers) == 0 {
  162. continue
  163. }
  164. regions = append(regions, Region{
  165. ID: rawRegion.ID, Name: rawRegion.Name, CountryCode: strings.ToUpper(rawRegion.Country), Geo: *rawRegion.Geo,
  166. PortForwarding: portForwarding, WireGuard: servers,
  167. })
  168. }
  169. if len(regions) == 0 {
  170. return nil, NewError(CodeCatalogSchemaUnsupported, "The PIA region list contains no available WireGuard regions.")
  171. }
  172. sort.Slice(regions, func(i, j int) bool {
  173. if regions[i].CountryCode == regions[j].CountryCode {
  174. return regions[i].Name < regions[j].Name
  175. }
  176. return regions[i].CountryCode < regions[j].CountryCode
  177. })
  178. return regions, nil
  179. }