package source import ( "fmt" "io" "log/slog" "net/http" "net/netip" "strings" "time" ) type Kind string const ( KindStatic Kind = "static" KindURL Kind = "url" KindFile Kind = "file" KindICanHazIp Kind = "icanhazip" ) type Family string const ( IPv4 Family = "ipv4" IPv6 Family = "ipv6" ) type Config struct { Kind Kind `json:"kind"` Family Family `json:"family,omitempty"` URL string `json:"url,omitempty"` Path string `json:"path,omitempty"` Value string `json:"value,omitempty"` } type Source interface { Supports(Family) bool Lookup(Family) (netip.Addr, error) } func New(log *slog.Logger, name string, cfg Config) (Source, error) { log = log.With( slog.Group( "source", slog.String("name", name), slog.String("kind", string(cfg.Kind)), ), ) if cfg.Kind == KindICanHazIp { return newICanHazIp(log) } // all sources below need an explicit family if cfg.Family != IPv4 && cfg.Family != IPv6 { return nil, fmt.Errorf("%s source needs family set to ipv4 or ipv6", cfg.Kind) } if cfg.Kind == KindStatic { return newStatic(log, cfg) } if cfg.Kind == KindURL { return newURL(log, cfg) } if cfg.Kind == KindFile { return newFile(log, cfg) } return nil, fmt.Errorf("unknown source kind %q", cfg.Kind) } // parsing function for ipv4 and ipv6 strings func parse(raw string, family Family) (netip.Addr, error) { addr, err := netip.ParseAddr(strings.TrimSpace(raw)) if err != nil { return netip.Addr{}, err } if addr.Is6() != (family == IPv6) { return netip.Addr{}, fmt.Errorf("%s is not an %s address", addr, family) } return addr, nil } // generic http fetching helper since a lot of sources will need it. func fetch(url string, family Family) (netip.Addr, error) { client := http.Client{Timeout: 15 * time.Second} resp, err := client.Get(url) if err != nil { return netip.Addr{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return netip.Addr{}, fmt.Errorf("%s: %s", url, resp.Status) } raw, err := io.ReadAll(io.LimitReader(resp.Body, 256)) if err != nil { return netip.Addr{}, err } return parse(string(raw), family) }