package allinkl import ( "context" "encoding/json" "fmt" "io" "net/http" "net/netip" "net/url" "strings" "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/ipextract" "github.com/qdm12/ddns-updater/pkg/publicip/ipversion" ) type Provider struct { domain string owner string ipVersion ipversion.IPVersion ipv6Suffix netip.Prefix username string password string } func New(data json.RawMessage, domain, owner string, ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) ( p *Provider, err error, ) { extraSettings := struct { Username string `json:"username"` Password string `json:"password"` }{} err = json.Unmarshal(data, &extraSettings) if err != nil { return nil, err } err = validateSettings(domain, owner, extraSettings.Username, extraSettings.Password) if err != nil { return nil, fmt.Errorf("validating provider specific settings: %w", err) } return &Provider{ domain: domain, owner: owner, ipVersion: ipVersion, ipv6Suffix: ipv6Suffix, username: extraSettings.Username, password: extraSettings.Password, }, nil } func validateSettings(domain, owner, username, password string) (err error) { err = utils.CheckDomain(domain) if err != nil { return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err) } switch { case owner == "*": return fmt.Errorf("%w", errors.ErrOwnerWildcard) case username == "": return fmt.Errorf("%w", errors.ErrUsernameNotSet) case password == "": return fmt.Errorf("%w", errors.ErrPasswordNotSet) } return nil } func (p *Provider) String() string { return utils.ToString(p.domain, p.owner, constants.AllInkl, 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: "ALL-INKL.com", 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: "dyndns.kasserver.com", Path: "/", User: url.UserPassword(p.username, p.password), } values := url.Values{} values.Set("host", utils.BuildURLQueryHostname(p.owner, p.domain)) if ip.Is6() { values.Set("myip6", ip.String()) } else { values.Set("myip", 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) response, err := client.Do(request) if err != nil { return netip.Addr{}, fmt.Errorf("doing http request: %w", err) } defer response.Body.Close() b, err := io.ReadAll(response.Body) if err != nil { return netip.Addr{}, fmt.Errorf("reading response body: %w", err) } s := string(b) if response.StatusCode != http.StatusOK { return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s)) } switch s { case "": return netip.Addr{}, fmt.Errorf("%w", errors.ErrReceivedNoResult) case constants.Nineoneone: return netip.Addr{}, fmt.Errorf("%w", errors.ErrDNSServerSide) case constants.Abuse: return netip.Addr{}, fmt.Errorf("%w", errors.ErrBannedAbuse) case "!donator": return netip.Addr{}, fmt.Errorf("%w", errors.ErrFeatureUnavailable) case constants.Badagent: return netip.Addr{}, fmt.Errorf("%w", errors.ErrBannedUserAgent) case constants.Badauth: return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth) case constants.Nohost: return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists) } if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") { return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s) } var ips []netip.Addr if ip.Is4() { ips = ipextract.IPv4(s) } else { ips = ipextract.IPv6(s) } if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w", errors.ErrReceivedNoIP) } newIP = ips[0] 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 }