package provider import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "log/slog" "net/http" "os" "path" "strings" "github.com/maximotejeda/ddns/internal/application/core/domain/cf" "gopkg.in/yaml.v3" ) var ( baseUrl = "https://api.cloudflare.com/client/v4/zones/" subPath = "/dns_records/" ) type cloudflareClient struct { ctx context.Context log *slog.Logger client http.Client token string zoneID string SelectdnsRecordID string name string settings *cf.Settings DnsRecords []cf.ResponseBody } // NewCloudflareClient // Create a new client for cloudflare API func NewCloudflareClient(ctx context.Context, log *slog.Logger, zoneID, token string) *cloudflareClient { c := cloudflareClient{} if zoneID == "" || token == "" { panic(errors.New("zoneid and token nedded to initiate client")) } log = log.With("location", "cloudflareClient") c.ctx = ctx c.log = log c.zoneID = zoneID c.token = token c.client = *http.DefaultClient return &c } // getConfig // Will determine the place where config is located // // ["~/.local/share/ddnser/config","./config"] func GetSettingsFile(loc string) (settings *cf.Settings) { location := "" p := "" fName := "settings.yaml" environ := os.Getenv("ENVIRONMENT") if environ != "prod" { p, _ = os.Getwd() location = path.Join(p, "config", fName) } else { p, _ = os.UserHomeDir() location = path.Join(p, ".local", "ddnser", "config", fName) } f, err := os.Open(location) if err != nil { panic(err) } decoder := yaml.NewDecoder(f) decoder.KnownFields(false) err = decoder.Decode(&settings) if err != nil { panic(err) } fmt.Printf("%v, error: %v\n", settings, err) return settings } // GenerateDefaultSettings // Generate default setting that can be saved oon json or yaml func (c *cloudflareClient) GenerateDefaultSettings() *cf.Settings { settings := cf.Settings{} zd := &settings zd.NSTTL = 86400 zd.ZoneMode = "standard" zd.NameServers.Type = "cloudflare.standard" soa := &zd.Soa soa.RName = "dns.cloudflare.com" soa.Refresh = 10000 soa.Retry = 2400 soa.Expire = 604800 soa.MinTTL = 1800 soa.TTL = 36000 c.settings = &settings return &settings } // List // List all the records for the ZoneID func (c *cloudflareClient) List() (result *cf.ResponseBody, err error) { c.log.Debug("Calling list") req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%s", baseUrl, c.zoneID, subPath), nil) if err != nil { panic(err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.client.Do(req) if err != nil { c.log.Error(err.Error()) } result = &cf.ResponseBody{} result, err = handleResult(c.log, resp, *result) if err != nil { c.log.Error("marshaling err", "error", err) } return result, err } // Export // Export dns recors for later import func (c *cloudflareClient) Export() ([]byte, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%sexport", baseUrl, c.zoneID, subPath), nil) if err != nil { panic(err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.client.Do(req) if err != nil { c.log.Error(err.Error()) c.log.Info(fmt.Sprintf("%#v", req)) return nil, err } //result := &domain.ReponseBody{} data, err := io.ReadAll(resp.Body) //err = json.Unmarshal(data, result) if err != nil { return nil, err } return data, nil } // Import // With an export file, import that file func (c *cloudflareClient) Import(file io.Reader) (err error) { req, err := http.NewRequest("POST", fmt.Sprintf("%s%s%simport", baseUrl, c.zoneID, subPath), file) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) // TODO // Import file and add it ass form data param named file // form data named proxied // ADD it to Request _, err = c.client.Do(req) if err != nil { c.log.Error(err.Error()) c.log.Info(fmt.Sprintf("%#v", req)) return err } return nil } // Update // update a current record // Param: // // {id} // {dns_params} func (c *cloudflareClient) Update(id string, reqBody *cf.RequestBody) (result *cf.DetailsResult, err error) { b, _ := json.Marshal(reqBody) buf := bytes.NewBuffer(b) req, err := http.NewRequest("PATCH", fmt.Sprintf("%s%s%s%s", baseUrl, c.zoneID, subPath, id), buf) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.client.Do(req) if err != nil { return nil, err } result = &cf.DetailsResult{} result, err = handleResult(c.log, resp, *result) return result, err } // Create // Create a single dns record with specified params func (c *cloudflareClient) Create(reqBody *cf.RequestBody) (result *cf.DetailsResult, err error) { b, _ := json.Marshal(reqBody) buf := bytes.NewBuffer(b) req, err := http.NewRequest("POST", fmt.Sprintf("%s%s%s", baseUrl, c.zoneID, subPath), buf) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.client.Do(req) if err != nil { c.log.Error(err.Error()) c.log.Info(fmt.Sprintf("%#v", req)) return nil, err } result = &cf.DetailsResult{} result, err = handleResult(c.log, resp, *result) return result, err } // Delete // Delete dns Recors expecified // Params: // // {id} func (c *cloudflareClient) Delete(id string) (result *cf.DetailsResult, err error) { req, err := http.NewRequest("DELETE", fmt.Sprintf("%s%s%s%s", baseUrl, c.zoneID, subPath, id), nil) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.client.Do(req) if err != nil { panic(err) } result = &cf.DetailsResult{} result, err = handleResult(c.log, resp, *result) return result, err } // Overwrite // Overwrite dns records with new params // Params: // // {id} // {comment} func (c *cloudflareClient) Overwrite(reqBody cf.RequestBody, id string) (result *cf.DetailsResult, err error) { b, _ := json.Marshal(reqBody) buf := bytes.NewBuffer(b) req, err := http.NewRequest("PUT", fmt.Sprintf("%s%s%s%s", baseUrl, c.zoneID, subPath, id), buf) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.client.Do(req) if err != nil { panic(err) } result = &cf.DetailsResult{} result, err = handleResult(c.log, resp, *result) return result, nil } // Details // Returns a single result for a dns record domain.DetailResult func (c *cloudflareClient) Details(id string) (result *cf.DetailsResult, err error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%s%s", baseUrl, c.zoneID, subPath, id), nil) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.client.Do(req) if err != nil { panic(err) } respBody, err := io.ReadAll(resp.Body) c.log.Debug(string(respBody)) if err != nil { panic(err) } result = &cf.DetailsResult{} err = json.Unmarshal(respBody, result) if err != nil { return nil, err } return result, err } // Selector func (c *cloudflareClient) Selector(subDomain, domain, comment, tipe string) (selRecord []*cf.Result, err error) { // Select the domains to update records, err := c.List() for _, record := range records.Result { if record.Type == strings.ToUpper(tipe) { params := strings.Split(record.Name, ".") if subDomain != "" { if len(params) == 3 { if strings.EqualFold(params[0], subDomain) { selRecord = append(selRecord, &record) } } } else { if strings.EqualFold(record.Name, domain) { selRecord = append(selRecord, &record) } } } if subDomain == "*" || domain == "*" { selRecord = append(selRecord, &record) } } return selRecord, nil } func handleResult[T cf.ResponseBody | cf.DetailsResult](log *slog.Logger, resp *http.Response, t T) (result *T, err error) { respBody, err := io.ReadAll(resp.Body) if err != nil { log.Error("[handleResult]->reading body", "error", err) return nil, err } defer resp.Body.Close() err = json.Unmarshal(respBody, &t) if err != nil { log.Error("[handleResult]-> unmarshal body ", "error", err, "body", respBody) return nil, err } return &t, err }