package njalla import ( "context" "encoding/json" "fmt" "net/http" "net/netip" "net/url" "github.com/qdm12/ddns-updater/internal/models" "github.com/qdm12/ddns-updater/internal/provider/constants" "github.com/qdm12/ddns-updater/internal/provider/errors" "github.com/qdm12/ddns-updater/internal/provider/headers" "github.com/qdm12/ddns-updater/internal/provider/utils" "github.com/qdm12/ddns-updater/pkg/publicip/ipversion" ) type Provider struct { domain string owner string ipVersion ipversion.IPVersion ipv6Suffix netip.Prefix key string } func New(data json.RawMessage, domain, owner string, ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) ( p *Provider, err error, ) { extraSettings := struct { Key string `json:"key"` }{} err = json.Unmarshal(data, &extraSettings) if err != nil { return nil, err } err = validateSettings(domain, extraSettings.Key) if err != nil { return nil, fmt.Errorf("validating provider specific settings: %w", err) } return &Provider{ domain: domain, owner: owner, ipVersion: ipVersion, ipv6Suffix: ipv6Suffix, key: extraSettings.Key, }, nil } func validateSettings(domain, key string) (err error) { err = utils.CheckDomain(domain) if err != nil { return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err) } if key == "" { return fmt.Errorf("%w", errors.ErrKeyNotSet) } return nil } func (p *Provider) String() string { return utils.ToString(p.domain, p.owner, constants.Njalla, p.ipVersion) } func (p *Provider) Domain() string { return p.domain } func (p *Provider) Owner() string { return p.owner } func (p *Provider) IPVersion() ipversion.IPVersion { return p.ipVersion } func (p *Provider) IPv6Suffix() netip.Prefix { return p.ipv6Suffix } func (p *Provider) Proxied() bool { return false } func (p *Provider) BuildDomainName() string { return utils.BuildDomainName(p.owner, p.domain) } func (p *Provider) HTML() models.HTMLRow { return models.HTMLRow{ Domain: fmt.Sprintf("%s", p.BuildDomainName(), p.BuildDomainName()), Owner: p.Owner(), Provider: "Njalla", IPVersion: p.ipVersion.String(), } } func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) { u := url.URL{ Scheme: "https", Host: "njal.la", Path: "/update", } values := url.Values{} values.Set("h", utils.BuildURLQueryHostname(p.owner, p.domain)) values.Set("k", p.key) updatingIP6 := ip.Is6() if updatingIP6 { values.Set("aaaa", ip.String()) } else { values.Set("a", ip.String()) } u.RawQuery = values.Encode() request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return netip.Addr{}, fmt.Errorf("creating http request: %w", err) } headers.SetUserAgent(request) headers.SetAccept(request, "application/json") response, err := client.Do(request) if err != nil { return netip.Addr{}, fmt.Errorf("doing http request: %w", err) } defer response.Body.Close() decoder := json.NewDecoder(response.Body) var respBody struct { Message string `json:"message"` Value struct { A string `json:"A"` AAAA string `json:"AAAA"` } `json:"value"` } err = decoder.Decode(&respBody) if err != nil { return netip.Addr{}, fmt.Errorf("json decoding response body: %w", err) } switch response.StatusCode { case http.StatusOK: if respBody.Message != "record updated" { return netip.Addr{}, fmt.Errorf("%w: message received: %s", errors.ErrUnknownResponse, respBody.Message) } ipString := respBody.Value.A if updatingIP6 { ipString = respBody.Value.AAAA } newIP, err = netip.ParseAddr(ipString) if err != nil { return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err) } else if ip.Compare(newIP) != 0 { return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s", errors.ErrIPReceivedMismatch, ip, newIP) } return newIP, nil case http.StatusUnauthorized: return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrAuth, respBody.Message) case http.StatusInternalServerError: return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrBadRequest, respBody.Message) } return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, respBody.Message) }