package updater import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/netip" "strings" "github.com/qdm12/gluetun/internal/provider/common" ) type apiData struct { Success bool `json:"success"` DataCenters []apiDataCenter `json:"datacenters"` } type apiDataCenter struct { City string `json:"city"` CountryName string `json:"country_name"` Servers []apiServer `json:"servers"` } type apiServer struct { IP netip.Addr `json:"ip"` Ptr string `json:"ptr"` // hostname Online bool `json:"online"` PublicKey string `json:"public_key"` WireguardPorts []uint16 `json:"wireguard_ports"` } func fetchAPI(ctx context.Context, client *http.Client) ( data apiData, err error, ) { const url = "https://www.ovpn.com/v2/api/client/entry" request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return data, err } response, err := client.Do(request) if err != nil { return data, err } if response.StatusCode != http.StatusOK { _ = response.Body.Close() return data, fmt.Errorf("%w: %d %s", common.ErrHTTPStatusCodeNotOK, response.StatusCode, response.Status) } decoder := json.NewDecoder(response.Body) err = decoder.Decode(&data) if err != nil { _ = response.Body.Close() return data, fmt.Errorf("decoding response body: %w", err) } err = response.Body.Close() if err != nil { return data, fmt.Errorf("closing response body: %w", err) } return data, nil } var ( ErrCityNotSet = errors.New("city is not set") ErrCountryNameNotSet = errors.New("country name is not set") ErrServersNotSet = errors.New("servers array is not set") ) func (a *apiDataCenter) validate() (err error) { conditionalErrors := []conditionalError{ {err: ErrCityNotSet, condition: a.City == ""}, {err: ErrCountryNameNotSet, condition: a.CountryName == ""}, {err: ErrServersNotSet, condition: len(a.Servers) == 0}, } err = collectErrors(conditionalErrors) if err != nil { var dataCenterSetFields []string if a.CountryName != "" { dataCenterSetFields = append(dataCenterSetFields, a.CountryName) } if a.City != "" { dataCenterSetFields = append(dataCenterSetFields, a.City) } if len(dataCenterSetFields) == 0 { return err } return fmt.Errorf("data center %s: %w", strings.Join(dataCenterSetFields, ", "), err) } for i, server := range a.Servers { err = server.validate() if err != nil { return fmt.Errorf("datacenter %s, %s: server %d of %d: %w", a.CountryName, a.City, i+1, len(a.Servers), err) } } return nil } var ( ErrIPFieldNotValid = errors.New("ip address is not set") ErrHostnameFieldNotSet = errors.New("hostname field is not set") ErrPublicKeyFieldNotSet = errors.New("public key field is not set") ErrWireguardPortsNotSet = errors.New("wireguard ports array is not set") ErrWireguardPortNotDefault = errors.New("wireguard port is not the default 9929") ) func (a *apiServer) validate() (err error) { const defaultWireguardPort = 9929 conditionalErrors := []conditionalError{ {err: ErrIPFieldNotValid, condition: !a.IP.IsValid()}, {err: ErrHostnameFieldNotSet, condition: a.Ptr == ""}, {err: ErrPublicKeyFieldNotSet, condition: a.PublicKey == ""}, {err: ErrWireguardPortsNotSet, condition: len(a.WireguardPorts) == 0}, { err: ErrWireguardPortNotDefault, condition: len(a.WireguardPorts) != 1 || a.WireguardPorts[0] != defaultWireguardPort, }, } err = collectErrors(conditionalErrors) switch { case err == nil: return nil case a.Ptr != "": return fmt.Errorf("server %s: %w", a.Ptr, err) case a.IP.IsValid(): return fmt.Errorf("server %s: %w", a.IP.String(), err) default: return err } } type conditionalError struct { err error condition bool } type joinedError struct { errs []error } func (e *joinedError) Unwrap() []error { return e.errs } func (e *joinedError) Error() string { errStrings := make([]string, len(e.errs)) for i, err := range e.errs { errStrings[i] = err.Error() } return strings.Join(errStrings, "; ") } func collectErrors(conditionalErrors []conditionalError) (err error) { errs := make([]error, 0, len(conditionalErrors)) for _, conditionalError := range conditionalErrors { if !conditionalError.condition { continue } errs = append(errs, conditionalError.err) } if len(errs) == 0 { return nil } return &joinedError{ errs: errs, } }