ddns/internal/adapter/provider/cloudflare.go
2024-11-13 16:44:46 -04:00

327 lines
8.4 KiB
Go

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
}