From 12fbd33dd13cac9696d3981fda65d3399d3160d6 Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Fri, 25 Apr 2025 21:15:01 +0200 Subject: [PATCH] feat: fix hetzner api provide example-config.yaml instead of config.yaml which is used for testing --- .gitignore | 1 + example-config.yaml | 17 ++++++ internal/provider/hetzner/hetzner.go | 91 +++++++++++++++++++--------- 3 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 .gitignore create mode 100644 example-config.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b6b072 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.yaml diff --git a/example-config.yaml b/example-config.yaml new file mode 100644 index 0000000..19605fc --- /dev/null +++ b/example-config.yaml @@ -0,0 +1,17 @@ +server: + bind_address: ":9090" + tls: + enabled: false + cert_file: "cert.pem" + key_file: "key.pem" +upstream: + provider: hetzner + hetzner: + api_token: "YOUR_HETZNER_API_TOKEN" +clients: + client1: + secret: "s3cr3t123" + exact: + - "home.example.com" + wildcard: + - "example.net" \ No newline at end of file diff --git a/internal/provider/hetzner/hetzner.go b/internal/provider/hetzner/hetzner.go index 79ae14a..72476f5 100644 --- a/internal/provider/hetzner/hetzner.go +++ b/internal/provider/hetzner/hetzner.go @@ -11,7 +11,7 @@ import ( pvd "git.cloonar.com/cloonar/updns/internal/provider" ) -const defaultAPIBase = "https://dns.hetzner.com" +const defaultAPIBase = "https://dns.hetzner.com/api/v1" type provider struct { token string @@ -29,10 +29,12 @@ type zonesResponse struct { } type record struct { - ID string `json:"id"` - Name string `json:"name"` - Value string `json:"value"` - Type string `json:"type"` + ID string `json:"id"` + Name string `json:"name"` + Value string `json:"value"` + Type string `json:"type"` + ZoneID string `json:"zone_id"` + TTL int `json:"ttl"` } type recordsResponse struct { @@ -62,10 +64,11 @@ func (p *provider) UpdateRecord(ctx context.Context, domain, ip string) error { } else { zoneName = strings.Join(parts[len(parts)-2:], ".") } + subdomain := strings.Join(parts[:len(parts)-2], ".") // Fetch zone ID zonesURL := fmt.Sprintf("%s/zones?name=%s", p.apiBaseURL, zoneName) req, _ := http.NewRequestWithContext(ctx, http.MethodGet, zonesURL, nil) - req.Header.Set("Authorization", "Bearer "+p.token) + req.Header.Set("Auth-API-Token", p.token) resp, err := p.client.Do(req) if err != nil { return fmt.Errorf("fetch zones: %w", err) @@ -86,7 +89,7 @@ func (p *provider) UpdateRecord(ctx context.Context, domain, ip string) error { // Fetch records in zone recsURL := fmt.Sprintf("%s/records?zone_id=%s", p.apiBaseURL, zoneID) req, _ = http.NewRequestWithContext(ctx, http.MethodGet, recsURL, nil) - req.Header.Set("Authorization", "Bearer "+p.token) + req.Header.Set("Auth-API-Token", p.token) resp, err = p.client.Do(req) if err != nil { return fmt.Errorf("fetch records: %w", err) @@ -101,32 +104,64 @@ func (p *provider) UpdateRecord(ctx context.Context, domain, ip string) error { } var recID string for _, rec := range rr.Records { - if rec.Name == domain { + if rec.Name == subdomain { recID = rec.ID break } } if recID == "" { - return fmt.Errorf("record %s not found", domain) - } + // return fmt.Errorf("record %s not found", domain) + // Create new record + // Cut the last 2 parts of the domain name + createURL := fmt.Sprintf("%s/records", p.apiBaseURL) + body := record{ + Name: subdomain, + Type: "A", + Value: ip, + TTL: 60, + ZoneID: zoneID, + } + buf := &bytes.Buffer{} + if err := json.NewEncoder(buf).Encode(body); err != nil { + return fmt.Errorf("encode create body: %w", err) + } + req, _ = http.NewRequestWithContext(ctx, http.MethodPost, createURL, buf) + req.Header.Set("Auth-API-Token", p.token) + req.Header.Set("Content-Type", "application/json") + resp, err = p.client.Do(req) + if err != nil { + return fmt.Errorf("create record: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("create API status: %s", resp.Status) + } + } else { + // Update record value + updateURL := fmt.Sprintf("%s/records/%s", p.apiBaseURL, recID) + body := record{ + Name: subdomain, + Type: "A", + Value: ip, + TTL: 60, + ZoneID: zoneID, + } + buf := &bytes.Buffer{} + if err := json.NewEncoder(buf).Encode(body); err != nil { + return fmt.Errorf("encode update body: %w", err) + } + req, _ = http.NewRequestWithContext(ctx, http.MethodPut, updateURL, buf) + req.Header.Set("Auth-API-Token", p.token) + req.Header.Set("Content-Type", "application/json") + resp, err = p.client.Do(req) + if err != nil { + return fmt.Errorf("update record: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("update API status: %s", resp.Status) + } + } - // Update record value - updateURL := fmt.Sprintf("%s/records/%s", p.apiBaseURL, recID) - body := map[string]string{"value": ip} - buf := &bytes.Buffer{} - if err := json.NewEncoder(buf).Encode(body); err != nil { - return fmt.Errorf("encode update body: %w", err) - } - req, _ = http.NewRequestWithContext(ctx, http.MethodPut, updateURL, buf) - req.Header.Set("Authorization", "Bearer "+p.token) - req.Header.Set("Content-Type", "application/json") - resp, err = p.client.Do(req) - if err != nil { - return fmt.Errorf("update record: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("update API status: %s", resp.Status) - } return nil }