diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index 2a8968d1..02ec772c 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -34,7 +34,7 @@ import ( "github.com/qdm12/gluetun/internal/pprof" "github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/publicip" - "github.com/qdm12/gluetun/internal/publicip/ipinfo" + pubipapi "github.com/qdm12/gluetun/internal/publicip/api" "github.com/qdm12/gluetun/internal/routing" "github.com/qdm12/gluetun/internal/server" "github.com/qdm12/gluetun/internal/shadowsocks" @@ -396,7 +396,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone) controlGroupHandler.Add(dnsTickerHandler) - ipFetcher := ipinfo.New(httpClient, *allSettings.PublicIP.APIToken) + ipFetcher, err := pubipapi.New(pubipapi.IPInfo, httpClient, *allSettings.PublicIP.APIToken) + if err != nil { + return fmt.Errorf("creating public IP API client: %w", err) + } publicIPLooper := publicip.NewLoop(ipFetcher, logger.New(log.SetComponent("ip getter")), allSettings.PublicIP, puid, pgid) diff --git a/internal/cli/openvpnconfig.go b/internal/cli/openvpnconfig.go index 6cbd9059..46a1a734 100644 --- a/internal/cli/openvpnconfig.go +++ b/internal/cli/openvpnconfig.go @@ -9,9 +9,9 @@ import ( "time" "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/openvpn/extract" "github.com/qdm12/gluetun/internal/provider" - "github.com/qdm12/gluetun/internal/publicip/ipinfo" "github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/updater/resolver" ) @@ -32,7 +32,7 @@ type ParallelResolver interface { } type IPFetcher interface { - FetchMultiInfo(ctx context.Context, ips []netip.Addr) (data []ipinfo.Response, err error) + FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error) } type IPv6Checker interface { diff --git a/internal/cli/update.go b/internal/cli/update.go index 1412ed2b..5e4cdcfd 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -14,7 +14,7 @@ import ( "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/openvpn/extract" "github.com/qdm12/gluetun/internal/provider" - "github.com/qdm12/gluetun/internal/publicip/ipinfo" + "github.com/qdm12/gluetun/internal/publicip/api" "github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/updater" "github.com/qdm12/gluetun/internal/updater/resolver" @@ -80,7 +80,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e httpClient := &http.Client{Timeout: clientTimeout} unzipper := unzip.New(httpClient) parallelResolver := resolver.NewParallelResolver(options.DNSAddress) - ipFetcher := ipinfo.New(httpClient, ipToken) + ipFetcher, err := api.New(api.IPInfo, httpClient, ipToken) + if err != nil { + return fmt.Errorf("creating public IP API client: %w", err) + } openvpnFileExtractor := extract.New() providers := provider.NewProviders(storage, time.Now, logger, httpClient, diff --git a/internal/provider/common/updater.go b/internal/provider/common/updater.go index 10225d19..f72bcaac 100644 --- a/internal/provider/common/updater.go +++ b/internal/provider/common/updater.go @@ -6,7 +6,6 @@ import ( "net/netip" "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/gluetun/internal/publicip/ipinfo" "github.com/qdm12/gluetun/internal/updater/resolver" ) @@ -34,5 +33,5 @@ type Warner interface { } type IPFetcher interface { - FetchMultiInfo(ctx context.Context, ips []netip.Addr) (data []ipinfo.Response, err error) + FetchInfo(ctx context.Context, ip netip.Addr) (result models.PublicIP, err error) } diff --git a/internal/provider/privado/updater/location.go b/internal/provider/privado/updater/location.go index 07c25b59..6b985b69 100644 --- a/internal/provider/privado/updater/location.go +++ b/internal/provider/privado/updater/location.go @@ -6,6 +6,7 @@ import ( "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/common" + "github.com/qdm12/gluetun/internal/publicip/api" ) func setLocationInfo(ctx context.Context, fetcher common.IPFetcher, servers []models.Server) (err error) { @@ -14,7 +15,7 @@ func setLocationInfo(ctx context.Context, fetcher common.IPFetcher, servers []mo for _, server := range servers { ipsToGetInfo = append(ipsToGetInfo, server.IPs...) } - ipsInfo, err := fetcher.FetchMultiInfo(ctx, ipsToGetInfo) + ipsInfo, err := api.FetchMultiInfo(ctx, fetcher, ipsToGetInfo) if err != nil { return err } diff --git a/internal/provider/purevpn/updater/servers.go b/internal/provider/purevpn/updater/servers.go index 7a09652d..76b80f36 100644 --- a/internal/provider/purevpn/updater/servers.go +++ b/internal/provider/purevpn/updater/servers.go @@ -9,6 +9,7 @@ import ( "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/common" + "github.com/qdm12/gluetun/internal/publicip/api" "github.com/qdm12/gluetun/internal/updater/openvpn" ) @@ -80,7 +81,7 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) ( for i := range servers { ipsToGetInfo[i] = servers[i].IPs[0] } - ipsInfo, err := u.ipFetcher.FetchMultiInfo(ctx, ipsToGetInfo) + ipsInfo, err := api.FetchMultiInfo(ctx, u.ipFetcher, ipsToGetInfo) if err != nil { return nil, err } diff --git a/internal/publicip/api/api.go b/internal/publicip/api/api.go new file mode 100644 index 00000000..9e86add0 --- /dev/null +++ b/internal/publicip/api/api.go @@ -0,0 +1,47 @@ +package api + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/netip" + "strings" + + "github.com/qdm12/gluetun/internal/models" +) + +type API interface { + FetchInfo(ctx context.Context, ip netip.Addr) ( + result models.PublicIP, err error) +} + +type Provider string + +const ( + IPInfo Provider = "ipinfo" +) + +func New(provider Provider, client *http.Client, token string) ( //nolint:ireturn + a API, err error) { + switch provider { + case IPInfo: + return newIPInfo(client, token), nil + default: + panic("provider not valid: " + provider) + } +} + +var ( + ErrProviderNotValid = errors.New("API name is not valid") +) + +func ParseProvider(s string) (provider Provider, err error) { + switch strings.ToLower(s) { + case "ipinfo": + return IPInfo, nil + default: + return "", fmt.Errorf(`%w: %q can only be "ipinfo" or "ip2location"`, + ErrProviderNotValid, s) + } +} diff --git a/internal/publicip/api/errors.go b/internal/publicip/api/errors.go new file mode 100644 index 00000000..7ef8f1fb --- /dev/null +++ b/internal/publicip/api/errors.go @@ -0,0 +1,9 @@ +package api + +import "errors" + +var ( + ErrTokenNotValid = errors.New("token is not valid") + ErrTooManyRequests = errors.New("too many requests sent for this month") + ErrBadHTTPStatus = errors.New("bad HTTP status received") +) diff --git a/internal/publicip/api/interfaces.go b/internal/publicip/api/interfaces.go new file mode 100644 index 00000000..76051d14 --- /dev/null +++ b/internal/publicip/api/interfaces.go @@ -0,0 +1,13 @@ +package api + +import ( + "context" + "net/netip" + + "github.com/qdm12/gluetun/internal/models" +) + +type Fetcher interface { + FetchInfo(ctx context.Context, ip netip.Addr) ( + result models.PublicIP, err error) +} diff --git a/internal/publicip/ipinfo/fetch.go b/internal/publicip/api/ipinfo.go similarity index 51% rename from internal/publicip/ipinfo/fetch.go rename to internal/publicip/api/ipinfo.go index 6f625c52..15b7eefe 100644 --- a/internal/publicip/ipinfo/fetch.go +++ b/internal/publicip/api/ipinfo.go @@ -1,40 +1,34 @@ -package ipinfo +package api import ( "context" "encoding/json" - "errors" "fmt" "net/http" "net/netip" "strings" "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" ) -type Fetch struct { +type ipInfo struct { client *http.Client token string } -func New(client *http.Client, token string) *Fetch { - return &Fetch{ +func newIPInfo(client *http.Client, token string) *ipInfo { + return &ipInfo{ client: client, token: token, } } -var ( - ErrTokenNotValid = errors.New("token is not valid") - ErrTooManyRequests = errors.New("too many requests sent for this month") - ErrBadHTTPStatus = errors.New("bad HTTP status received") -) - // FetchInfo obtains information on the ip address provided // using the ipinfo.io API. If the ip is the zero value, the public IP address // of the machine is used as the IP. -func (f *Fetch) FetchInfo(ctx context.Context, ip netip.Addr) ( - result Response, err error) { +func (i *ipInfo) FetchInfo(ctx context.Context, ip netip.Addr) ( + result models.PublicIP, err error) { url := "https://ipinfo.io/" switch { case ip.Is6(): @@ -47,15 +41,15 @@ func (f *Fetch) FetchInfo(ctx context.Context, ip netip.Addr) ( if err != nil { return result, err } - request.Header.Set("Authorization", "Bearer "+f.token) + request.Header.Set("Authorization", "Bearer "+i.token) - response, err := f.client.Do(request) + response, err := i.client.Do(request) if err != nil { return result, err } defer response.Body.Close() - if f.token != "" && response.StatusCode == http.StatusUnauthorized { + if i.token != "" && response.StatusCode == http.StatusUnauthorized { return result, fmt.Errorf("%w: %s", ErrTokenNotValid, response.Status) } @@ -70,15 +64,37 @@ func (f *Fetch) FetchInfo(ctx context.Context, ip netip.Addr) ( } decoder := json.NewDecoder(response.Body) - if err := decoder.Decode(&result); err != nil { + var data struct { + IP netip.Addr `json:"ip,omitempty"` + Region string `json:"region,omitempty"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Hostname string `json:"hostname,omitempty"` + Loc string `json:"loc,omitempty"` + Org string `json:"org,omitempty"` + Postal string `json:"postal,omitempty"` + Timezone string `json:"timezone,omitempty"` + } + if err := decoder.Decode(&data); err != nil { return result, fmt.Errorf("decoding response: %w", err) } - countryCode := strings.ToLower(result.Country) + countryCode := strings.ToLower(data.Country) country, ok := constants.CountryCodes()[countryCode] if ok { - result.Country = country + data.Country = country } + result = models.PublicIP{ + IP: data.IP, + Region: data.Region, + Country: data.Country, + City: data.City, + Hostname: data.Hostname, + Location: data.Loc, + Organization: data.Org, + PostalCode: data.Postal, + Timezone: data.Timezone, + } return result, nil } diff --git a/internal/publicip/ipinfo/multi.go b/internal/publicip/api/multi.go similarity index 77% rename from internal/publicip/ipinfo/multi.go rename to internal/publicip/api/multi.go index fe83708c..d9f020c9 100644 --- a/internal/publicip/ipinfo/multi.go +++ b/internal/publicip/api/multi.go @@ -1,8 +1,10 @@ -package ipinfo +package api import ( "context" "net/netip" + + "github.com/qdm12/gluetun/internal/models" ) // FetchMultiInfo obtains the public IP address information for every IP @@ -11,13 +13,13 @@ import ( // If an error is encountered, all the operations are canceled and // an error is returned, so the results returned should be considered // incomplete in this case. -func (f *Fetch) FetchMultiInfo(ctx context.Context, ips []netip.Addr) ( - results []Response, err error) { +func FetchMultiInfo(ctx context.Context, fetcher Fetcher, ips []netip.Addr) ( + results []models.PublicIP, err error) { ctx, cancel := context.WithCancel(ctx) type asyncResult struct { index int - result Response + result models.PublicIP err error } resultsCh := make(chan asyncResult) @@ -27,12 +29,12 @@ func (f *Fetch) FetchMultiInfo(ctx context.Context, ips []netip.Addr) ( aResult := asyncResult{ index: index, } - aResult.result, aResult.err = f.FetchInfo(ctx, ip) + aResult.result, aResult.err = fetcher.FetchInfo(ctx, ip) resultsCh <- aResult }(i, ip) } - results = make([]Response, len(ips)) + results = make([]models.PublicIP, len(ips)) for i := 0; i < len(ips); i++ { aResult := <-resultsCh if aResult.err != nil { diff --git a/internal/publicip/interfaces.go b/internal/publicip/interfaces.go index 4027d72f..db14f1f5 100644 --- a/internal/publicip/interfaces.go +++ b/internal/publicip/interfaces.go @@ -4,12 +4,12 @@ import ( "context" "net/netip" - "github.com/qdm12/gluetun/internal/publicip/ipinfo" + "github.com/qdm12/gluetun/internal/models" ) type Fetcher interface { FetchInfo(ctx context.Context, ip netip.Addr) ( - result ipinfo.Response, err error) + result models.PublicIP, err error) } type Logger interface { diff --git a/internal/publicip/ipinfo/model.go b/internal/publicip/ipinfo/model.go deleted file mode 100644 index 3c5c2080..00000000 --- a/internal/publicip/ipinfo/model.go +++ /dev/null @@ -1,33 +0,0 @@ -package ipinfo - -import ( - "net/netip" - - "github.com/qdm12/gluetun/internal/models" -) - -type Response struct { - IP netip.Addr `json:"ip,omitempty"` - Region string `json:"region,omitempty"` - Country string `json:"country,omitempty"` - City string `json:"city,omitempty"` - Hostname string `json:"hostname,omitempty"` - Loc string `json:"loc,omitempty"` - Org string `json:"org,omitempty"` - Postal string `json:"postal,omitempty"` - Timezone string `json:"timezone,omitempty"` -} - -func (r *Response) ToPublicIPModel() (model models.PublicIP) { - return models.PublicIP{ - IP: r.IP, - Region: r.Region, - Country: r.Country, - City: r.City, - Hostname: r.Hostname, - Location: r.Loc, - Organization: r.Org, - PostalCode: r.Postal, - Timezone: r.Timezone, - } -} diff --git a/internal/publicip/loop.go b/internal/publicip/loop.go index 97c715f0..e862485f 100644 --- a/internal/publicip/loop.go +++ b/internal/publicip/loop.go @@ -10,7 +10,7 @@ import ( "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/gluetun/internal/publicip/ipinfo" + "github.com/qdm12/gluetun/internal/publicip/api" ) type Loop struct { @@ -103,7 +103,7 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{}, lastFetch = l.timeNow() timerIsReadyToReset = l.updateTimer(*l.settings.Period, lastFetch, timer, timerIsReadyToReset) - if errors.Is(err, ipinfo.ErrTooManyRequests) { + if errors.Is(err, api.ErrTooManyRequests) { continue } @@ -112,7 +112,7 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{}, l.logger.Info(message) l.ipDataMutex.Lock() - l.ipData = result.ToPublicIPModel() + l.ipData = result l.ipDataMutex.Unlock() filepath := *l.settings.IPFilepath @@ -123,7 +123,7 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{}, } } -func (l *Loop) fetchIPData(ctx context.Context) (result ipinfo.Response, err error) { +func (l *Loop) fetchIPData(ctx context.Context) (result models.PublicIP, err error) { // keep retrying since settings updates won't change the // behavior of the following code. const defaultBackoffTime = 5 * time.Second @@ -135,7 +135,7 @@ func (l *Loop) fetchIPData(ctx context.Context) (result ipinfo.Response, err err return result, nil case ctx.Err() != nil: return result, err - case errors.Is(err, ipinfo.ErrTooManyRequests): + case errors.Is(err, api.ErrTooManyRequests): l.logger.Warn(err.Error() + "; not retrying.") return result, err }