diff options
Diffstat (limited to 'source')
| -rw-r--r-- | source/file.go | 59 | ||||
| -rw-r--r-- | source/source.go | 91 | ||||
| -rw-r--r-- | source/static.go | 44 | ||||
| -rw-r--r-- | source/url.go | 49 |
4 files changed, 243 insertions, 0 deletions
diff --git a/source/file.go b/source/file.go new file mode 100644 index 0000000..4ef0e7c --- /dev/null +++ b/source/file.go @@ -0,0 +1,59 @@ +package source + +import ( + "errors" + "fmt" + "log/slog" + "net/netip" + "os" +) + +// a source that reads the address from a file path. +// useful when your record has to be in-sync with +// some configured address on your machine. +type fileSource struct { + log *slog.Logger + path string + family Family +} + +var _ Source = (*fileSource)(nil) + +func newFile(log *slog.Logger, cfg Config) (*fileSource, error) { + log = log.With(slog.String("source", "file")) + + if _, err := os.Stat(cfg.Path); errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("cannot access file for source: %w", err) + } + + return &fileSource{log: log, path: cfg.Path, family: cfg.Family}, nil +} + +func (s *fileSource) Supports(f Family) bool { return f == s.family } + +func (s *fileSource) Lookup(Family) (netip.Addr, error) { + s.log.Info( + "will look up address from file source", + slog.String("path", s.path), + slog.String("family", string(s.family)), + ) + + raw, err := os.ReadFile(s.path) + if err != nil { + return netip.Addr{}, fmt.Errorf("file source read failed: %w", err) + } + content := string(raw) + + addr, err := parse(content, s.family) + if err != nil { + s.log.Error( + "source file does not contain address of expected family", + slog.String("path", s.path), + slog.String("expected", string(s.family)), + slog.String("content", content), + ) + return netip.Addr{}, fmt.Errorf("source file contains bad content: %w", err) + } + + return addr, nil +} diff --git a/source/source.go b/source/source.go new file mode 100644 index 0000000..6fe139c --- /dev/null +++ b/source/source.go @@ -0,0 +1,91 @@ +package source + +import ( + "fmt" + "io" + "log/slog" + "net/http" + "net/netip" + "strings" + "time" +) + +type Kind string + +const ( + // only the simple generic sources for now! + KindStatic Kind = "static" + KindURL Kind = "url" + KindFile Kind = "file" +) + +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, cfg Config) (Source, error) { + 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) +} diff --git a/source/static.go b/source/static.go new file mode 100644 index 0000000..d2645d6 --- /dev/null +++ b/source/static.go @@ -0,0 +1,44 @@ +package source + +import ( + "fmt" + "log/slog" + "net/netip" +) + +// a simple source that returns the configured address. +// useful when a record needs to have one specific value, +// that is known in advance, and for debugging. +type staticSource struct { + log *slog.Logger + addr netip.Addr +} + +var _ Source = (*staticSource)(nil) + +func newStatic(log *slog.Logger, cfg Config) (*staticSource, error) { + log = log.With("source", "static") + addr, err := parse(cfg.Value, cfg.Family) + if err != nil { + log.Error( + "static source can't be created from value", + slog.String("family", string(cfg.Family)), + slog.String("value", cfg.Value), + ) + return nil, fmt.Errorf("can't create static source from value: %w", err) + } + + return &staticSource{log: log, addr: addr}, nil +} + +func (s *staticSource) Supports(f Family) bool { + return (s.addr.Is4() && f == IPv4) || (s.addr.Is6() && f == IPv6) +} + +func (s *staticSource) Lookup(Family) (netip.Addr, error) { + s.log.Info( + "looked up address from static source", + slog.String("addr", s.addr.String()), + ) + return s.addr, nil +} diff --git a/source/url.go b/source/url.go new file mode 100644 index 0000000..067d48b --- /dev/null +++ b/source/url.go @@ -0,0 +1,49 @@ +package source + +import ( + "fmt" + "log/slog" + "net/netip" + nurl "net/url" +) + +// a simple source that fetches an address from a url. +// sends a http get request to url, and parses whatever it found there. +// can be used to fetch address values from generic "my ip" +// services which don't have direct support. +// note that the address does have to be returned in plain-text! +type urlSource struct { + log *slog.Logger + url *nurl.URL + family Family +} + +var _ Source = (*urlSource)(nil) + +func newURL(log *slog.Logger, cfg Config) (*urlSource, error) { + log = log.With("source", "url") + + url, err := nurl.ParseRequestURI(cfg.URL) + if err != nil { + return nil, fmt.Errorf("invalid url for source: %w", err) + } + + return &urlSource{log: log, url: url, family: cfg.Family}, nil +} + +func (s *urlSource) Supports(f Family) bool { return f == s.family } + +func (s *urlSource) Lookup(Family) (netip.Addr, error) { + s.log.Info( + "will look up address via fetching from url", + slog.String("url", s.url.String()), + slog.String("family", string(s.family)), + ) + + addr, err := fetch(s.url.String(), s.family) + if err != nil { + return netip.Addr{}, fmt.Errorf("failed to source address from url: %w", err) + } + + return addr, nil +} |
