initial code
This commit is contained in:
185
internal/provider/ovpn/updater/api.go
Normal file
185
internal/provider/ovpn/updater/api.go
Normal file
@@ -0,0 +1,185 @@
|
||||
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"`
|
||||
MultiHopOpenvpnPort uint16 `json:"multihop_openvpn_port"`
|
||||
MultiHopWireguardPort uint16 `json:"multihop_wireguard_port"`
|
||||
}
|
||||
|
||||
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")
|
||||
ErrMultiHopOpenVPNPortNotSet = errors.New("multihop OpenVPN port is not set")
|
||||
ErrMultiHopWireguardPortNotSet = errors.New("multihop WireGuard port is not set")
|
||||
)
|
||||
|
||||
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: ErrMultiHopOpenVPNPortNotSet, condition: a.MultiHopOpenvpnPort == 0},
|
||||
{err: ErrMultiHopWireguardPortNotSet, condition: a.MultiHopWireguardPort == 0},
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user