180 lines
4.4 KiB
Go
180 lines
4.4 KiB
Go
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,
|
|
}
|
|
}
|