diff --git a/commands/set_acme_record.go b/commands/set_acme_record.go new file mode 100644 index 0000000..6a6aec7 --- /dev/null +++ b/commands/set_acme_record.go @@ -0,0 +1,120 @@ +package commands + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type SetAcmeRecordArgs struct { + APIToken string + ZoneID string + RecordName string + RecordValue string +} + +type cfDNSRecordRequest struct { + Type string `json:"type"` + Name string `json:"name"` + Content string `json:"content"` + TTL int `json:"ttl"` +} + +type cfDNSRecord struct { + ID string `json:"id"` + Name string `json:"name"` + Content string `json:"content"` +} + +type cfDNSRecordResponse struct { + Success bool `json:"success"` + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + Result cfDNSRecord `json:"result"` +} + +type cfDNSListResponse struct { + Success bool `json:"success"` + Errors []struct { + Message string `json:"message"` + } `json:"errors"` + Result []cfDNSRecord `json:"result"` +} + +func cfRequest(method, url, token string, body []byte) (*http.Response, error) { + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return (&http.Client{}).Do(req) +} + +func SetAcmeRecord(args SetAcmeRecordArgs, stdout, stderr io.Writer) error { + listURL := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=TXT&name=%s", args.ZoneID, args.RecordName) + resp, err := cfRequest("GET", listURL, args.APIToken, nil) + if err != nil { + return fmt.Errorf("error listing DNS records: %w", err) + } + defer resp.Body.Close() + + var listResult cfDNSListResponse + if err := json.NewDecoder(resp.Body).Decode(&listResult); err != nil { + return fmt.Errorf("error decoding list response: %w", err) + } + if !listResult.Success { + msg := "Cloudflare API error:" + for _, e := range listResult.Errors { + msg += " " + e.Message + } + return fmt.Errorf("%s", msg) + } + + record := cfDNSRecordRequest{ + Type: "TXT", + Name: args.RecordName, + Content: args.RecordValue, + TTL: 1, + } + body, err := json.Marshal(record) + if err != nil { + return fmt.Errorf("error marshaling request: %w", err) + } + + var method, recordURL, action string + if len(listResult.Result) > 0 { + method = "PUT" + recordURL = fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", args.ZoneID, listResult.Result[0].ID) + action = "Updated" + } else { + method = "POST" + recordURL = fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", args.ZoneID) + action = "Created" + } + + resp2, err := cfRequest(method, recordURL, args.APIToken, body) + if err != nil { + return fmt.Errorf("error calling Cloudflare API: %w", err) + } + defer resp2.Body.Close() + + var result cfDNSRecordResponse + if err := json.NewDecoder(resp2.Body).Decode(&result); err != nil { + return fmt.Errorf("error decoding response: %w", err) + } + if !result.Success { + msg := "Cloudflare API error:" + for _, e := range result.Errors { + msg += " " + e.Message + } + return fmt.Errorf("%s", msg) + } + + fmt.Fprintf(stdout, "%s TXT record:\n ID: %s\n Name: %s\n Content: %s\n", + action, result.Result.ID, result.Result.Name, result.Result.Content) + return nil +}