package integration import ( "context" "encoding/json" "net" "sort" "strconv" "strings" "time" "github.com/mhsanaei/3x-ui/v3/internal/crypto/nodetoken" piaprotocol "github.com/mhsanaei/3x-ui/v3/internal/pia" "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard" "github.com/mhsanaei/3x-ui/v3/internal/web/service" ) var piaTokenAAD = []byte("settings/pia_token") type PiaService struct { service.SettingService Auth piaprotocol.Authenticator Catalog *piaprotocol.Catalog Registrar piaprotocol.Registrar } type piaStored struct { Username string `json:"username"` Token string `json:"token"` TokenExpiresAt int64 `json:"tokenExpiresAt"` } type PiaAccountView struct { Username string `json:"username"` AccountHint string `json:"accountHint"` } type PiaCountryView struct { Code string `json:"code"` } type PiaRegionView struct { ID string `json:"id"` Name string `json:"name"` } type PiaServerView struct { Hostname string `json:"hostname"` IP string `json:"ip"` RegionID string `json:"regionId"` RegionName string `json:"regionName"` } type PiaServersView struct { Regions []PiaRegionView `json:"regions"` Servers []PiaServerView `json:"servers"` } type PiaKeyView struct { Tag string `json:"tag"` Hostname string `json:"hostname"` SecretKey string `json:"secretKey"` Address string `json:"address"` PublicKey string `json:"publicKey"` Endpoint string `json:"endpoint"` } func NewPiaService() *PiaService { return &PiaService{ Auth: piaprotocol.NewAuthClient(piaprotocol.DefaultTokenEndpoint), Catalog: piaprotocol.NewCatalog(piaprotocol.NewCatalogClient(piaprotocol.DefaultServerListEndpoint, piaprotocol.EmbeddedServerListPublicKey)), Registrar: piaprotocol.NewRegistrationClient(piaprotocol.EmbeddedPIACA), } } func (s *PiaService) Login(username, password string) (*PiaAccountView, error) { tok, err := s.Auth.Authenticate(context.Background(), username, []byte(password)) if err != nil { return nil, err } stored := piaStored{ Username: strings.TrimSpace(username), Token: string(tok.Value), TokenExpiresAt: tok.ExpiresAt.Unix(), } if err := s.saveStored(stored); err != nil { return nil, err } return accountView(stored.Username), nil } func (s *PiaService) GetPiaData() (*PiaAccountView, error) { stored, err := s.loadStored() if err != nil { return nil, err } if stored == nil || stored.Token == "" { return nil, nil } return accountView(stored.Username), nil } func (s *PiaService) DelPiaData() error { return s.SetPia("") } func (s *PiaService) GetCountries() ([]PiaCountryView, error) { regions, err := s.regions() if err != nil { return nil, err } seen := map[string]struct{}{} out := make([]PiaCountryView, 0) for _, region := range regions { code := strings.ToUpper(strings.TrimSpace(region.CountryCode)) if !validCountryCode(code) { continue } if _, ok := seen[code]; ok { continue } seen[code] = struct{}{} out = append(out, PiaCountryView{Code: code}) } sort.Slice(out, func(i, j int) bool { return out[i].Code < out[j].Code }) return out, nil } func (s *PiaService) GetServers(countryCode string) (*PiaServersView, error) { code := strings.ToUpper(strings.TrimSpace(countryCode)) if !validCountryCode(code) { return nil, piaprotocol.NewError(piaprotocol.CodeInvalidInput, "Select a country.") } regions, err := s.regions() if err != nil { return nil, err } view := &PiaServersView{Regions: []PiaRegionView{}, Servers: []PiaServerView{}} for _, region := range regions { if strings.ToUpper(strings.TrimSpace(region.CountryCode)) != code { continue } view.Regions = append(view.Regions, PiaRegionView{ID: region.ID, Name: region.Name}) for _, server := range region.WireGuard { view.Servers = append(view.Servers, PiaServerView{ Hostname: server.Hostname, IP: server.IP.String(), RegionID: region.ID, RegionName: region.Name, }) } } sort.Slice(view.Regions, func(i, j int) bool { return view.Regions[i].Name < view.Regions[j].Name }) return view, nil } func (s *PiaService) AddKey(hostname string) (*PiaKeyView, error) { hostname = strings.TrimSpace(hostname) if hostname == "" { return nil, piaprotocol.NewError(piaprotocol.CodeInvalidInput, "Select a PIA server.") } stored, err := s.loadStored() if err != nil { return nil, err } if stored == nil || stored.Token == "" { return nil, piaprotocol.NewError(piaprotocol.CodeTokenRejected, "Sign in with a PIA account first.") } if stored.TokenExpiresAt > 0 && time.Now().Unix() >= stored.TokenExpiresAt { return nil, piaprotocol.NewError(piaprotocol.CodeTokenRejected, "The PIA token has expired. Sign in again.") } region, server, err := s.findServer(hostname) if err != nil { return nil, err } priv, pub, err := wireguard.GenerateWireguardKeypair() if err != nil { return nil, err } reg, err := s.Registrar.RegisterKey(context.Background(), server, stored.Token, pub) if err != nil { return nil, err } return &PiaKeyView{ Tag: piaOutboundTag(region.ID, server.Hostname), Hostname: server.Hostname, SecretKey: priv, Address: reg.PeerIP.String(), PublicKey: reg.ServerKey, Endpoint: net.JoinHostPort(reg.ServerIP.String(), strconv.Itoa(int(reg.ServerPort))), }, nil } func (s *PiaService) regions() ([]piaprotocol.Region, error) { if s.Catalog == nil { return nil, piaprotocol.NewError(piaprotocol.CodeCatalogUnavailable, "The PIA server list is not available.") } regions, _, err := s.Catalog.ListRegions(context.Background()) return regions, err } func (s *PiaService) findServer(hostname string) (piaprotocol.Region, piaprotocol.WireGuardServer, error) { regions, err := s.regions() if err != nil { return piaprotocol.Region{}, piaprotocol.WireGuardServer{}, err } for _, region := range regions { for _, server := range region.WireGuard { if server.Hostname == hostname || piaOutboundTag(region.ID, server.Hostname) == hostname { return region, server, nil } } } return piaprotocol.Region{}, piaprotocol.WireGuardServer{}, piaprotocol.NewError(piaprotocol.CodeServerNotFound, "The selected PIA server was not found.") } func piaOutboundTag(regionID, hostname string) string { region := piaTagPart(regionID, false) server := piaTagPart(hostname, true) if region == "" { return "pia-" + server } return "pia-" + region + "-" + server } func piaTagPart(s string, stripDomain bool) string { s = strings.ToLower(strings.TrimSpace(s)) if stripDomain { if i := strings.IndexByte(s, '.'); i > 0 { s = s[:i] } } return strings.ReplaceAll(s, "_", "-") } func (s *PiaService) saveStored(stored piaStored) error { enc, err := nodetoken.EncryptBound(piaTokenAAD, stored.Token) if err != nil { return err } stored.Token = enc raw, err := json.Marshal(stored) if err != nil { return err } return s.SetPia(string(raw)) } func (s *PiaService) loadStored() (*piaStored, error) { raw, err := s.GetPia() if err != nil || strings.TrimSpace(raw) == "" { return nil, err } var stored piaStored if err := json.Unmarshal([]byte(raw), &stored); err != nil { return nil, err } atRest := stored.Token if atRest == "" { return &stored, nil } if nodetoken.IsEncrypted(atRest) && !nodetoken.Enabled() { return nil, piaprotocol.NewError(piaprotocol.CodeTokenRejected, "The PIA token is encrypted but NODE_TOKEN_ENCRYPTION is off. Sign in again.") } plain, err := nodetoken.DecryptBound(piaTokenAAD, atRest) if err != nil { return nil, err } stored.Token = plain if nodetoken.Enabled() && (!nodetoken.IsEncrypted(atRest) || !nodetoken.Active().EncryptedWithActive(atRest)) { if err := s.saveStored(stored); err != nil { return nil, err } } return &stored, nil } func accountView(username string) *PiaAccountView { return &PiaAccountView{Username: username, AccountHint: piaAccountHint(username)} } func piaAccountHint(username string) string { u := strings.TrimSpace(username) if len(u) <= 4 { return strings.Repeat("*", len(u)) } return u[:2] + strings.Repeat("*", len(u)-4) + u[len(u)-2:] } func validCountryCode(code string) bool { if len(code) != 2 { return false } return code[0] >= 'A' && code[0] <= 'Z' && code[1] >= 'A' && code[1] <= 'Z' }