327 lines
8.4 KiB
Go
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
|
|
}
|