From d96142b3482c56c6c95c58ccfc5aadeaaa4d792a Mon Sep 17 00:00:00 2001 From: maximo tejeda Date: Wed, 13 Nov 2024 16:44:46 -0400 Subject: [PATCH] FIRST COMMIT --- .dir-locals.el | 0 .gitignore | 2 + Makefile | 14 + README.org | 65 ++++ cmd/main.go | 101 ++++++ config/config.bak | 17 + config/config.go | 26 ++ config/settings.yaml | 16 + go.mod | 7 + go.sum | 6 + internal/adapter/checker/checker.go | 95 +++++ internal/adapter/provider/cloudflare.go | 326 ++++++++++++++++++ internal/adapter/provider/cloudflare_test.go | 14 + internal/api/domain/cf/cloudflare.go | 123 +++++++ internal/application/core/api/api.go | 304 ++++++++++++++++ .../application/core/domain/cf/cloudflare.go | 124 +++++++ internal/application/core/domain/provider.go | 20 ++ internal/application/core/helpers/helpers.go | 102 ++++++ internal/port/checker.go | 5 + internal/port/provider.go | 19 + 20 files changed, 1386 insertions(+) create mode 100644 .dir-locals.el create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.org create mode 100644 cmd/main.go create mode 100644 config/config.bak create mode 100644 config/config.go create mode 100644 config/settings.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/adapter/checker/checker.go create mode 100644 internal/adapter/provider/cloudflare.go create mode 100644 internal/adapter/provider/cloudflare_test.go create mode 100644 internal/api/domain/cf/cloudflare.go create mode 100644 internal/application/core/api/api.go create mode 100644 internal/application/core/domain/cf/cloudflare.go create mode 100644 internal/application/core/domain/provider.go create mode 100644 internal/application/core/helpers/helpers.go create mode 100644 internal/port/checker.go create mode 100644 internal/port/provider.go diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1800c7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.env +bin/* \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e3780d1 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +include .env +export + +.phony: all clean build test clean-image build-image build-image-debug run-image run-image-debug run-local + +run-local:clean build + mkdir db + @bin/ddnser +build: + @go build -o ./bin/ddnser ./cmd +test: + @go -count=1 test ./... +clean: + @rm -rf ./bin ./db diff --git a/README.org b/README.org new file mode 100644 index 0000000..90d6e82 --- /dev/null +++ b/README.org @@ -0,0 +1,65 @@ +Working creating Request +#+begin_src shell +curl --request POST \ + --url https://api.cloudflare.com/client/v4/zones/b4d1481be7a5d3460768b9bd84d74bfd/dns_records \ + --header 'Content-Type: application/json' \ + --header "Authorization: Bearer 5LgXMn4GjuVG125RoapHUE_uTh8SjnIwVQr-3w0A" \ + --data '{ + "comment": "Domain verification record", + "name": "maximotejeda.com", + "proxied": true, + "settings": {}, + "tags": [], + "ttl": 3600, + "content": "200.88.135.2", + "type": "A" +}' +#+end_src + +#+RESULTS: +| {"result":{"id":"63aa045481a5fe258ed77dad985c09b5","zone_id":"b4d1481be7a5d3460768b9bd84d74bfd","zone_name":"maximotejeda.com","name":"maximotejeda.com","type":"A","content":"200.88.135.2","proxiable":true,"proxied":true,"ttl":1,"settings":{},"meta":{"auto_added":false,"managed_by_apps":false,"managed_by_argo_tunnel":false},"comment":"Domain verification record","tags":[],"created_on":"2024-09-24T16:11:45.71887Z","modified_on":"2024-09-24T16:11:45.71887Z","comment_modified_on":"2024-09-24T16:11:45.71887Z"},"success":true,"errors":[],"messages":[]} | | | + +Query zones dns actives +#+begin_src shell + curl --request GET \ + --url https://api.cloudflare.com/client/v4/zones/b4d1481be7a5d3460768b9bd84d74bfd/dns_records \ + --header "Authorization: Bearer 5LgXMn4GjuVG125RoapHUE_uTh8SjnIwVQr-3w0A" \ + --header 'Content-Type: application/json' \ +#+end_src + +#+RESULTS: +| {"result":[{"id":"63aa045481a5fe258ed77dad985c09b5" | zone_id:"b4d1481be7a5d3460768b9bd84d74bfd" | zone_name:"maximotejeda.com" | name:"maximotejeda.com" | type:"A" | content:"200.88.135.2" | proxiable:true | proxied:true | ttl:1 | settings:{} | meta:{"auto_added":false | managed_by_apps:false | managed_by_argo_tunnel:false} | comment:"Domain verification record" | tags:[] | created_on:"2024-09-24T16:11:45.71887Z" | modified_on:"2024-09-24T16:11:45.71887Z" | comment_modified_on:"2024-09-24T16:11:45.71887Z"}] | success:true | errors:[] | messages:[] | result_info:{"page":1 | per_page:100 | count:1 | total_count:1 | total_pages:1}} | + + + +update zone +#+begin_src shell + curl --request PATCH \ + --url https://api.cloudflare.com/client/v4/zones/b4d1481be7a5d3460768b9bd84d74bfd/dns_records/63aa045481a5fe258ed77dad985c09b5 \ + --header 'Content-Type: application/json' \ + --header "Authorization: Bearer 5LgXMn4GjuVG125RoapHUE_uTh8SjnIwVQr-3w0A" \ + --data '{ + "comment": "Domain verification record", + "name": "maximotejeda.com", + "proxied": true, + "settings": {}, + "tags": [], + "ttl": 3600, + "content": "200.88.135.2", + "type": "A" + }' +#+end_src + +#+RESULTS: +| {"result":{"id":"63aa045481a5fe258ed77dad985c09b5" | zone_id:"b4d1481be7a5d3460768b9bd84d74bfd" | zone_name:"maximotejeda.com" | name:"maximotejeda.com" | type:"A" | content:"200.88.135.2" | proxiable:true | proxied:true | ttl:1 | settings:{} | meta:{"auto_added":false | managed_by_apps:false | managed_by_argo_tunnel:false} | comment:"Domain verification record" | tags:[] | created_on:"2024-09-24T16:11:45.71887Z" | modified_on:"2024-09-24T16:11:45.71887Z" | comment_modified_on:"2024-09-24T16:11:45.71887Z"} | success:true | errors:[] | messages:[]} | + +check config for a zone +#+begin_src shell + curl --request GET \ + --url https://api.cloudflare.com/client/v4/zones/b4d1481be7a5d3460768b9bd84d74bfd/dns_settings \ + --header 'Content-Type: application/json' \ + --header "Authorization: Bearer 5LgXMn4GjuVG125RoapHUE_uTh8SjnIwVQr-3w0A" \ +#+end_src + +#+RESULTS: +| {"result":{"nameservers":{"type":"cloudflare.standard"} | foundation_dns:false | multi_provider:false | secondary_overrides:false | soa:{"mname":null | rname:"dns.cloudflare.com" | refresh:10000 | retry:2400 | expire:604800 | min_ttl:1800 | ttl:3600} | ns_ttl:86400 | zone_mode:"standard" | internal_dns:{"reference_zone_id":null} | flatten_all_cnames:false} | success:true | errors:[] | messages:[]} | diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..68ceef0 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "os" + + "github.com/maximotejeda/ddns/internal/application/core/api" +) + +var ( + tok string + zid string + name string + ip string + tipe string + operation string + proxied bool + helpMsg string + provider string +rID string +) + +func init() { + flag.BoolVar(&proxied, "p", false, "Proxie request (shothand)") + flag.BoolVar(&proxied, "proxied", false, "Proxie request") + flag.StringVar(&name, "n", "", "REQUIRED: name subdomain (shorthand)") + flag.StringVar(&name, "name", "", "REQUIRED: name subdomain") + flag.StringVar(&ip, "ip", "", "ip to update record") + flag.StringVar(&tipe, "t", "", "type of record (shorthand)") + flag.StringVar(&tipe, "type", "", "type of record ") + flag.StringVar(&operation, "o", "show", "operation to execute (shorthand)") + flag.StringVar(&operation, "operation", "show", "operation to execute") + flag.StringVar(&tok, "tk", "", "authorization token on service (shorthand)") + flag.StringVar(&tok, "token", "", "authorization token on service") + flag.StringVar(&zid, "zid", "", "zone id for record (shorthand)") + flag.StringVar(&zid, "zone-id", "", "zone id for record") + flag.StringVar(&provider, "pv", "cf", "Dns records provider") + flag.StringVar(&rID, "rid", "", "Select record id (shorthand)") + flag.StringVar(&rID, "record-id", "", "Select record id") + +} + +func main() { + flag.Parse() + lvl := new(slog.LevelVar) + switch os.Getenv("ENV"){ + case "debug", "dev": + lvl.Set(slog.LevelDebug) + case "prod": + lvl.Set(slog.LevelInfo) + } + + log := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})) + ctx := context.Background() + helpmsg := "required flag not provided " + if tok == "" { + tok = os.Getenv("TOKEN") + if tok == "" { + fmt.Print(printHelp()) + fmt.Print(helpmsg + "[Token]\n") + flag.Usage() + os.Exit(1) + } + } + if zid == "" { + zid = os.Getenv("ZONEID") + if zid == "" { + fmt.Print(printHelp()) + fmt.Print(helpmsg + "[zone id]\n") + flag.Usage() + os.Exit(1) + } + } + if name == "" { + name = os.Getenv("NAME") + if name == "" { + fmt.Print(printHelp()) + fmt.Print(helpmsg + "[domain name]\n") + flag.Usage() + os.Exit(1) + } + } + + app, err := api.NewApplication(ctx, log, provider, tok, zid) + if err != nil { + fmt.Println(zid) + panic(err) + } + app.Operation(operation, name, tipe, ip, proxied, rID) + +} + +func printHelp() string { + return ` +Ddnser Client +Service client for cloudflare API +` +} diff --git a/config/config.bak b/config/config.bak new file mode 100644 index 0000000..1a49663 --- /dev/null +++ b/config/config.bak @@ -0,0 +1,17 @@ +zone_defaults: + flatten_all_cnames: false + foundation_dns: false + multi_provider: false + ns_ttl: 86400 + secondary_overrides: false + zone_mode: "dns_only" +nameservers: + tipe: "cloudflare.standard" +soa: + expire: 604800 + min_ttl: 1800 + mname: "kristina.ns.cloudflare.com" + refresh: 10000 + retry: 2400 + rname: "admin.example.com" + ttl: 3500 diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..e21c18b --- /dev/null +++ b/config/config.go @@ -0,0 +1,26 @@ +package config + +import ( + "errors" + "fmt" + "os" +) + +var ( + NotFoundError = errors.New("env variable not set") +) + +func getEnv(key string) string { + if key == "" { + panic(fmt.Errorf("%w : key -> %s", NotFoundError, key)) + } + return os.Getenv(key) +} + +func GetDomainProvider(provider string) string { + return getEnv(provider) +} + +func GetServiceVerifyURL(verifyIPURL string) string { + return getEnv(verifyIPURL) +} diff --git a/config/settings.yaml b/config/settings.yaml new file mode 100644 index 0000000..5b850a9 --- /dev/null +++ b/config/settings.yaml @@ -0,0 +1,16 @@ +flatten_all_cnames: false +foundation_dns: false +multi_provider: false +ns_ttl: 86400 +secondary_overrides: false +zone_mode: "dns_only" +nameservers: + tipe: "cloudflare.standard" +soa: + expire: 604800 + min_ttl: 1800 + mname: "kristina.ns.cloudflare.com" + refresh: 10000 + retry: 2400 + rname: "admin.example.com" + ttl: 3500 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5470e15 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/maximotejeda/ddns + +go 1.22 + +require gopkg.in/yaml.v3 v3.0.1 + +require github.com/chmike/domain v1.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..29a89f5 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/chmike/domain v1.1.0 h1:615mGyA/ghxvIFBdAaYuB2azxAsUxrpm6Cv5UiL6VPo= +github.com/chmike/domain v1.1.0/go.mod h1:h558M2qGKpYRUxHHNyey6puvXkZBjvjmseOla/d1VGQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/adapter/checker/checker.go b/internal/adapter/checker/checker.go new file mode 100644 index 0000000..9492925 --- /dev/null +++ b/internal/adapter/checker/checker.go @@ -0,0 +1,95 @@ +// Package that will query for IP +package checker + +import ( + "errors" + "io" + "log/slog" + "net" + "net/http" + "strings" + + "github.com/maximotejeda/ddns/config" +) + +type client struct { + urls []string + method string + headers map[string][]string + reqs []*http.Request + log *slog.Logger +} + +type Response struct { + IP *net.IP `json:"ip,omitempty" yaml:"ip,omitempty"` + IPSTR string `json:"ip_str,omitempty" yaml:"ip_str,omitempty"` + Version string `json:"version" yaml:"version,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Identifyer string `json:"identifyer" yaml:"identifyer"` +} + +func NewClient(log *slog.Logger, provider string) *client { + c := &client{} + c.urls = strings.Split(config.GetServiceVerifyURL("IP_VERIFY_PROVIDER_URL"), ",") + c.method = http.MethodGet + for _, url := range c.urls { + req, _ := http.NewRequest(c.method, url, nil) + c.reqs = append(c.reqs, req) + } + log = log.With("location", "Checker") + c.log = log + return c +} + +// Do +// Make a request to retrieve the ip of the machine +func (c *client) Do() (res *Response, err error) { + r := &Response{} + client := http.DefaultClient + if len(c.reqs) <= 0 { + c.log.Error("no request on struct") + return nil, errors.New("no request on client struct") + } + resp, err := client.Do(c.reqs[0]) + r.Identifyer = c.urls[0] + if err != nil { + c.log.Error("doing request for ip", "err", err, "identidyer", c.reqs[0]) + c.log.Info("retrying on other provider") + resp, err = client.Do(c.reqs[1]) + r.Identifyer = c.urls[1] + if err != nil { + c.log.Error("doing request for ip", "err", err, "identidyer", c.reqs[1]) + return nil, err + } + } + defer resp.Body.Close() + + ipSTR, err := io.ReadAll(resp.Body) + if err != nil { + c.log.Error("reading body", err) + } + + ipi := net.ParseIP(string(ipSTR)) + r.IP = &ipi + + //check ip is not nil + if r.IP == nil { + return nil, errors.New("ip not found") + } + // check if ip is private + if r.IP.IsPrivate() { + return nil, errors.New("ip is a private ip") + } + // check if is loop back ip + if r.IP.IsLoopback() { + return nil, errors.New("ip is a loopback ip") + } + if r.IP.To4() != nil { + r.Version = "V4" + r.Type = "A" + }else { + r.Version = "V6" + r.Type = "AAAA" + } + return r, err +} diff --git a/internal/adapter/provider/cloudflare.go b/internal/adapter/provider/cloudflare.go new file mode 100644 index 0000000..e3d1b29 --- /dev/null +++ b/internal/adapter/provider/cloudflare.go @@ -0,0 +1,326 @@ +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 +} diff --git a/internal/adapter/provider/cloudflare_test.go b/internal/adapter/provider/cloudflare_test.go new file mode 100644 index 0000000..492bfd3 --- /dev/null +++ b/internal/adapter/provider/cloudflare_test.go @@ -0,0 +1,14 @@ +package provider + +import ( + "os" + "testing" +) + +func TestGetSettings(t *testing.T) { + os.Setenv("ENVIRONMENT", "dev") + s := GetSettingsFile("") + if s != nil { + t.Errorf("empty settings: %v", s) + } +} diff --git a/internal/api/domain/cf/cloudflare.go b/internal/api/domain/cf/cloudflare.go new file mode 100644 index 0000000..bca2fce --- /dev/null +++ b/internal/api/domain/cf/cloudflare.go @@ -0,0 +1,123 @@ +package cf + +type RequestBody struct { + Comment string `yaml:"comment" json:"comment,omitempty"` + DomainName string `json:"name" yaml:"name"` // domain name example.com", + Proxied bool `json:"proxied" yaml:"proxied"` //true, + Settings Settings `json:"settings,omitempty" yaml:"settings"` //{}, + Tags []string `json:"tags,omitempty" yaml:"tags"` //[], + TTL int `json:"ttl" yaml:"ttl"` //3600, + Content string `json:"content" yaml:"content"` //ip to add "198.51.100.4", + Type string `json:"type" yaml:"type"` //record type "A" +} + +type ResponseBody struct { + Errors []Error `json:"errors"` //"errors": [], + Messages []string `json:"messages"` //"messages": [], + Success bool `json:"success"` //"success": true, + Result []Result `json:"result"` // +} + +type Result struct { + ID string `json:"ID" yaml:"ID"` //"id": "023e105f4ecef8ad9ca31a8372d0c353", + ZoneID string `json:"zone_id" yaml:"zone_id"` + ZoneName string `json:"zone_name" yaml:"zone_name"` // the base name for domain.com + Comment string `json:"comment" yaml:"comment"` // "comment": "Domain verification record", + Name string `json:"name" yaml:"name"` //domain name or sub domain "name": "test.example.com", + Proxied bool `json:"proxied" yaml:"proxied"` //"proxied": true, + Settings Settings `json:"settings" yaml:"settings"` //"settings": {}, + Tags []string `json:"tags" yaml:"tags"` //"tags": [], + TTL int `json:"ttl" yaml:"ttl"` //"ttl": 3600, + Content string `json:"content" yaml:"content"` //IP "content": "198.51.100.4", + Type string `json:"type" yaml:"type"` //"type": "A", + CommentModifiedOn string `json:"comment_modified_on" yaml:"comment_modified_on"` //"comment_modified_on": "2024-01-01T05:20:00.12345Z", + CreatedOn string `json:"created_on" yaml:"created_on"` //"created_on": "2014-01-01T05:20:00.12345Z", + + Meta Meta `json:"meta" yaml:"meta"` //"meta": {}, + ModifiedOn string `json:"modified_on" yaml:"modified_on"` //"modified_on": "2014-01-01T05:20:00.12345Z", + Proxiable bool `json:"proxiable" yaml:"proxiable"` //"proxiable": true, + TagsModifiedOn string `json:"tags_modified_on" yaml:"tags_modified_on"` //"tags_modified_on": "2025-01-01T05:20:00.12345Z" + ResultInfo ResultInfo `json:"result_info" yaml:"result_info"` +} + +type DetailsResult struct { + Errors []Error `json:"errors"` //"errors": [], + Messages []string `json:"messages"` //"messages": [], + Success bool `json:"success"` //"success": true, + Result Result `json:"result" yaml:"result"` +} + +type Meta struct { + AutoAdded bool `json:"auto_added" yaml:"auto_added"` + ManagedByApps bool `json:"managed_by_apps" yaml:"managed_by_apps"` + ManagedByArgoTunnel bool `json:"managed_by_argo_tunnel" yaml:"managed_by_argo_tunnel"` +} + +type ResultInfo struct { + Page int `json:"page" yaml:"page"` + PerPage int `json:"per_page" yaml:"per_page"` + Count int `json:"count" yaml:"count"` + TotalCount int `json:"total_count" yaml:"total_count"` + TotalPages int `json:"total_pages" yaml:"total_pages"` +} + +// settings response example https://developers.cloudflare.com/api/operations/dns-settings-for-an-account-update-dns-settings +/* + "messages": [], + "success": true, + "result": { + "zone_defaults": { + "flatten_all_cnames": false, + "foundation_dns": false, + "multi_provider": false, + "nameservers": { + "type": "cloudflare.standard" + }, + "ns_ttl": 86400, + "secondary_overrides": false, + "soa": { + "expire": 604800, + "min_ttl": 1800, + "mname": "kristina.ns.cloudflare.com", + "refresh": 10000, + "retry": 2400, + "rname": "admin.example.com", + "ttl": 3600 + }, + "zone_mode": "dns_only" + } + } +} +*/ +type Settings1 struct { +} + +type Settings struct { + FlattenAllCNames bool `json:"flatten_all_cnames" yaml:"flatten_all_cnames"` //"flatten_all_cnames": false, + FoundationDns bool `json:"foundation_dns" yaml:"foundation_dns"` //"foundation_dns": false, + MultiProvider bool `json:"multi_provider" yaml:"multi_provider"` //"multi_provider": false, + NSTTL int `json:"ns_ttl" yaml:"ns_ttl"` //"ns_ttl": 86400, + SecondaryOverrides bool `json:"secondary_overrides" yaml:"secondary_overrides"` //"secondary_overrides": false, + ZoneMode string `json:"zone_mode" yaml:"zone_mode"` //"zone_mode": "dns_only" + NameServers NameServers `json:"nameservers" yaml:"nameservers"` + Soa SOA `json:"soa" yaml:"soa"` +} + +type NameServers struct { + Type string `json:"type" yaml:"tipe"` +} + +type SOA struct { + Expire int `json:"expire" yaml:"expire,flow"` //"expire": 604800, + MinTTL int `json:"min_ttl" yaml:"min_ttl,flow"` //"min_ttl": 1800, + MName string `json:"mname" yaml:"mname"` //"mname": "kristina.ns.cloudflare.com", + Refresh int `json:"refresh" yaml:"refresh"` //"refresh": 10000, + Retry int `json:"retry" yaml:"retry"` //"retry": 2400, + RName string `json:"rname" yaml:"rname"` //"rname": "admin.example.com", + TTL int `json:"ttl" yaml:"ttl"` //"ttl": 3600 +} + +type Error struct { + Code int `json:"code" yaml:"code"` + Message string `json:"message" yaml:"message"` +} diff --git a/internal/application/core/api/api.go b/internal/application/core/api/api.go new file mode 100644 index 0000000..74dc389 --- /dev/null +++ b/internal/application/core/api/api.go @@ -0,0 +1,304 @@ +package api + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net" + "slices" + "strings" + + "github.com/maximotejeda/ddns/internal/adapter/checker" + prov "github.com/maximotejeda/ddns/internal/adapter/provider" + "github.com/maximotejeda/ddns/internal/application/core/domain" + "github.com/maximotejeda/ddns/internal/application/core/domain/cf" + "github.com/maximotejeda/ddns/internal/application/core/helpers" + "github.com/maximotejeda/ddns/internal/port" +) + +type Application struct { + ctx context.Context + log *slog.Logger + client port.Provider + publicIP *net.IP + zoneRecords []cf.Result + Domains map[string]domain.DomainRecords +} + +// NewApplication +// Returns an instance to a new app with a provider client +// Needs some info from the account to work on +// Params: +// +// provider: provider client to interact with +// toke: token to authorize actions on provider +// id: id of user or zoner where to work with +func NewApplication(ctx context.Context, log *slog.Logger, provider, token, id string) (app *Application, err error) { + log.Debug("creating new appication") + var pClient port.Provider + switch provider { + case "cf": + pClient = prov.NewCloudflareClient(ctx, log, id, token) + } + logApp := log.With("location", "[Application]") + app = &Application{ + ctx: ctx, + log: logApp, + client: pClient, + } + return app, nil +} + +// Show +// Print in stdout the dns records for zone +func (app *Application) Show(name string) { + app.log.Debug("calling show") + fmt.Printf("%22s %5s \t%14s \t%s\n\n", "name", "type", "ip", "ID") + for _, it := range app.zoneRecords { + subDomainName := strings.ReplaceAll(strings.ReplaceAll(it.Name, name, ""), ".", "") + if subDomainName == "" { + subDomainName = "*root*" + } + fmt.Printf("%+22s %5s \t%14s \t%s\n", subDomainName, it.Type, it.Content, it.ID) + } + checkClient := checker.NewClient(app.log, "cf") + ip, err := checkClient.Do() + if err != nil { + app.log.Error("[checker] ip", "error", err) + } + fmt.Printf("\nrequester IP => %s\n", ip) +} + +// Details ... +// Print details on a given record +func (app *Application) Details(id string) { + app.log.Debug("calling details") + resp, err := app.client.Details(id) + if err != nil { + app.log.Error("[datails]", "error", err) + } + fmt.Printf(` +Zone Name: %s - ID: %s - Domain Name: %s +Type: %s - IP: %s - Proxied: %t +Comment: %s +Created On: %s +Modified On: %s + +Meta: %#v +Tags: %#v +TTL: %d + +`, + resp.Result.ZoneName, resp.Result.ID, resp.Result.Name, resp.Result.Type, + resp.Result.Content, resp.Result.Proxied, resp.Result.Comment, + resp.Result.CreatedOn, resp.Result.ModifiedOn, resp.Result.Meta, + resp.Result.Tags, resp.Result.TTL) +} + +// List +func (app *Application) List() { + resp, err := app.client.List() + if err != nil { + app.log.Error("[Show]", "error", err) + panic(err) + } + app.zoneRecords = resp.Result +} + +// Update +func (app *Application) Update(re *cf.Result, rBody cf.RequestBody) { + rBody.Comment = "updating from app cli tool" + res, err := app.client.Update(re.ID, &rBody) + if err != nil { + app.log.Error("[Update]", "error", err) + } + app.log.Debug("updating", "record", re.Name, "IP", rBody.Content, "Type", re.Type, "ID", "re.ID") + app.log.Info("response", "success", res.Success, "errors", res.Errors) +} + +// Create +func (app *Application) Create(rBody cf.RequestBody) (res *cf.DetailsResult, err error) { + rBody.Comment = "Creating from app cli tool" + res, err = app.client.Create(&rBody) + if err != nil { + app.log.Error("[Create]", "error", err) + } + return res, err +} + +// Overwrite +func (app *Application) Overwrite(re *cf.Result, rBody *cf.RequestBody) { + rBody.Comment = "Overwrite from app cli tool" + res, err := app.client.Overwrite(*rBody, re.ID) + if err != nil { + app.log.Error("[Overwrite]", "error", err) + } + fmt.Println("Overwrite:") + fmt.Printf("%v", res) +} + +// Delete +func (app Application) Delete(re *cf.Result) { + res, err := app.client.Delete(re.ID) + if err != nil { + app.log.Error("[Delete]", "error", err) + } + fmt.Println("Delete: ") + fmt.Printf("%v", res) +} + +// Operation +// expose required +func (app *Application) Operation(op string, name, tipo, ipSTR string, proxied bool, rID string) { + var ( + res *checker.Response + err error + rBody *cf.RequestBody + ) + // check current ip + func() { + checker := checker.NewClient(app.log, "cf") + res, err = checker.Do() + if err != nil { + app.log.Error("checking public ip", "error", err) + panic(err) + } + }() + + if ipSTR == "" { + app.publicIP = res.IP + tipo = res.Type + + } else { + ip1 := net.ParseIP(ipSTR) + app.publicIP = &ip1 + if ip1.To4() != nil { + tipo = "A" + } else if ip1.To16() != nil { + tipo = "AAA" + } + } + // populate struct with dns records + // and populate subdomains inside each domain + app.List() + app.Domains, _ = helpers.SubdomainIdentify(name, app.zoneRecords) + + dn := helpers.SelectDomain(name, app.Domains) // will always be available if subdomain is valid + rs := app.SelectRecord(dn, tipo) + if rs == nil { + app.log.Debug("record not selected", "ip", ipSTR, "name", name, "type", tipo) + } + //fmt.Println(dn, rs, app.zoneRecords) + app.Domains, _ = helpers.SubdomainIdentify(name, app.zoneRecords) + if res == nil { + fmt.Printf("%#v", app.Domains) + panic("record does not exist") + } + + switch op { + case "update": + if name == "" { + panic("name cant be empty for op update") + } + if rs == nil { + if dn == "*" { + for _, rec := range app.zoneRecords { + if rec.Content != app.publicIP.String() { + rBody = app.GenerateReqBody(rec.Name, res.Type, app.publicIP.String(), proxied) + app.Update(&rec, *rBody) + } else { + app.log.Info("same ip for", "record", rec.Name, "DstIP", rec.Content, "NewIP", app.publicIP) + } + } + } else { + app.log.Error(fmt.Sprintf("record is not created or active create first: %s -> %s", name, dn)) + } + } else { + if rs.Content != app.publicIP.String() { + rBody = app.GenerateReqBody(name, res.Type, app.publicIP.String(), proxied) + app.Update(rs, *rBody) + } else { + app.log.Error("same ip on dns record") + } + } + case "create": + // as we can have more than one subdomain/with the same name and diff ip + if name == "" { + panic("name cant be empty for op create") + } + fmt.Printf("domain name: %s", dn) + rBody = app.GenerateReqBody(dn, tipo, app.publicIP.String(), proxied) + app.Create(*rBody) + case "delete": + if name == "" { + panic("name cant be empty for op delete") + } + if rs == nil { + app.log.Error(fmt.Sprintf("could not find record: %s -> %s", name, dn), "operation", "delete", "rs", rs) + } else { + app.Delete(rs) + + } + case "overwrite": + if name == "" { + app.log.Error("name cant be empty for op overwrite") + } + if rs == nil { + + app.log.Error(fmt.Sprintf("could not find record: %s -> %s", name, dn), "operation", "overwrite") + } else { + rBody = app.GenerateReqBody(dn, tipo, app.publicIP.String(), proxied) + app.Overwrite(rs, rBody) + } + case "details": + if name == "" { + app.log.Error("name cant be empty for op details") + } + + if rs != nil { + rBody = app.GenerateReqBody(dn, tipo, app.publicIP.String(), proxied) + app.Details(rs.ID) + } + + //rBody := app.GenerateReqBody(name, tipo, app.publicIP.String(), proxied) + if rs == nil { + app.log.Error("record does not exist") + } + + case "show": + app.Show(name) + default: + app.log.Error("unknown operation", "operation", op) + panic("unknown operation") + } +} + +// SelectRecord +func (app *Application) SelectRecord(name, tipo string) (res *cf.Result) { + + res, _ = app.SelectNameAndType(name, tipo) + return res +} + +// SelectNameAndType +func (app *Application) SelectNameAndType(name, tipo string) (rec *cf.Result, err error) { + i := slices.IndexFunc(app.zoneRecords, func(r cf.Result) bool { + return name == r.Name && strings.ToUpper(tipo) == r.Type + }) + if i >= 0 { + return &app.zoneRecords[i], nil + } else { + return nil, errors.New("record not found") + } +} + +func (app *Application) GenerateReqBody(name, tipo, ipSTR string, proxied bool) (rBody *cf.RequestBody) { + rBody = &cf.RequestBody{} + rBody.DomainName = name + rBody.Type = tipo + rBody.Content = ipSTR + rBody.Proxied = proxied + rBody.TTL = 3600 + return rBody +} diff --git a/internal/application/core/domain/cf/cloudflare.go b/internal/application/core/domain/cf/cloudflare.go new file mode 100644 index 0000000..06bde1c --- /dev/null +++ b/internal/application/core/domain/cf/cloudflare.go @@ -0,0 +1,124 @@ +package cf + +type RequestBody struct { + Comment string `yaml:"comment" json:"comment,omitempty"` + DomainName string `json:"name" yaml:"name"` // domain name example.com", + Proxied bool `json:"proxied" yaml:"proxied"` //true, + Settings Settings `json:"settings,omitempty" yaml:"settings"` //{}, + Tags []string `json:"tags,omitempty" yaml:"tags"` //[], + TTL int `json:"ttl" yaml:"ttl"` //3600, + Content string `json:"content" yaml:"content"` //ip to add "198.51.100.4", + Type string `json:"type" yaml:"type"` //record type "A" +} + +type ResponseBody struct { + Errors []Error `json:"errors"` //"errors": [], + Messages []string `json:"messages"` //"messages": [], + Success bool `json:"success"` //"success": true, + Result []Result `json:"result"` // +} + +type Result struct { + ID string `json:"ID" yaml:"ID"` //"id": "023e105f4ecef8ad9ca31a8372d0c353", + ZoneID string `json:"zone_id" yaml:"zone_id"` + ZoneName string `json:"zone_name" yaml:"zone_name"` // the base name for domain.com + Comment string `json:"comment" yaml:"comment"` // "comment": "Domain verification record", + Name string `json:"name" yaml:"name"` //domain name or sub domain "name": "test.example.com", + Proxied bool `json:"proxied" yaml:"proxied"` //"proxied": true, + Settings Settings `json:"settings" yaml:"settings"` //"settings": {}, + Tags []string `json:"tags" yaml:"tags"` //"tags": [], + TTL int `json:"ttl" yaml:"ttl"` //"ttl": 3600, + Content string `json:"content" yaml:"content"` //IP "content": "198.51.100.4", + Type string `json:"type" yaml:"type"` //"type": "A", + CommentModifiedOn string `json:"comment_modified_on" yaml:"comment_modified_on"` //"comment_modified_on": "2024-01-01T05:20:00.12345Z", + CreatedOn string `json:"created_on" yaml:"created_on"` //"created_on": "2014-01-01T05:20:00.12345Z", + + Meta Meta `json:"meta" yaml:"meta"` //"meta": {}, + ModifiedOn string `json:"modified_on" yaml:"modified_on"` //"modified_on": "2014-01-01T05:20:00.12345Z", + Proxiable bool `json:"proxiable" yaml:"proxiable"` //"proxiable": true, + TagsModifiedOn string `json:"tags_modified_on" yaml:"tags_modified_on"` //"tags_modified_on": "2025-01-01T05:20:00.12345Z" + ResultInfo ResultInfo `json:"result_info" yaml:"result_info"` +} +type DetailsResult struct { + Errors []Error `json:"errors"` //"errors": [], + Messages []string `json:"messages"` //"messages": [], + Success bool `json:"success"` //"success": true, + Result Result `json:"result" yaml:"result"` +} + +type Meta struct { + AutoAdded bool `json:"auto_added" yaml:"auto_added"` + ManagedByApps bool `json:"managed_by_apps" yaml:"managed_by_apps"` + ManagedByArgoTunnel bool `json:"managed_by_argo_tunnel" yaml:"managed_by_argo_tunnel"` +} + +type ResultInfo struct { + Page int `json:"page" yaml:"page"` + PerPage int `json:"per_page" yaml:"per_page"` + Count int `json:"count" yaml:"count"` + TotalCount int `json:"total_count" yaml:"total_count"` + TotalPages int `json:"total_pages" yaml:"total_pages"` +} + +// settings response example https://developers.cloudflare.com/api/operations/dns-settings-for-an-account-update-dns-settings +/* + "messages": [], + "success": true, + "result": { + "zone_defaults": { + "flatten_all_cnames": false, + "foundation_dns": false, + "multi_provider": false, + "nameservers": { + "type": "cloudflare.standard" + }, + "ns_ttl": 86400, + "secondary_overrides": false, + "soa": { + "expire": 604800, + "min_ttl": 1800, + "mname": "kristina.ns.cloudflare.com", + "refresh": 10000, + "retry": 2400, + "rname": "admin.example.com", + "ttl": 3600 + }, + "zone_mode": "dns_only" + } + } +} +*/ + +type Settings struct { + FlattenAllCNames bool `json:"flatten_all_cnames" yaml:"flatten_all_cnames"` //"flatten_all_cnames": false, + FoundationDns bool `json:"foundation_dns" yaml:"foundation_dns"` //"foundation_dns": false, + MultiProvider bool `json:"multi_provider" yaml:"multi_provider"` //"multi_provider": false, + NSTTL int `json:"ns_ttl" yaml:"ns_ttl"` //"ns_ttl": 86400, + SecondaryOverrides bool `json:"secondary_overrides" yaml:"secondary_overrides"` //"secondary_overrides": false, + ZoneMode string `json:"zone_mode" yaml:"zone_mode"` //"zone_mode": "dns_only" + NameServers NameServers `json:"nameservers" yaml:"nameservers"` + Soa SOA `json:"soa" yaml:"soa"` +} + +type NameServers struct { + Type string `json:"type" yaml:"tipe"` +} + +type SOA struct { + Expire int `json:"expire" yaml:"expire,flow"` //"expire": 604800, + MinTTL int `json:"min_ttl" yaml:"min_ttl,flow"` //"min_ttl": 1800, + MName string `json:"mname" yaml:"mname"` //"mname": "kristina.ns.cloudflare.com", + Refresh int `json:"refresh" yaml:"refresh"` //"refresh": 10000, + Retry int `json:"retry" yaml:"retry"` //"retry": 2400, + RName string `json:"rname" yaml:"rname"` //"rname": "admin.example.com", + TTL int `json:"ttl" yaml:"ttl"` //"ttl": 3600 +} + +type Test struct { + Name string `yaml:"name"` + Age int `yaml:"age"` +} +type Error struct { + Code int `json:"code" yaml:"code"` + Message string `json:"message" yaml:"message"` +} diff --git a/internal/application/core/domain/provider.go b/internal/application/core/domain/provider.go new file mode 100644 index 0000000..dbd364b --- /dev/null +++ b/internal/application/core/domain/provider.go @@ -0,0 +1,20 @@ +package domain + +import "fmt" + +type DomainRecords struct { + Domain string `json:"root_domain" yaml:"root_domain"` + Existing []Record `json:"existing" yaml:"existing"` + ToCreate []Record `json:"to_create" yaml:"to_create"` +} + +type Record struct { + ID string + Name string + Content string + Type string +} + +func (r *Record) String() string { + return fmt.Sprintf("%s:%s", r.Name, r.Type) +} diff --git a/internal/application/core/helpers/helpers.go b/internal/application/core/helpers/helpers.go new file mode 100644 index 0000000..8ea89c0 --- /dev/null +++ b/internal/application/core/helpers/helpers.go @@ -0,0 +1,102 @@ +package helpers + +import ( + "fmt" + "slices" + "strings" + + "github.com/chmike/domain" + dm "github.com/maximotejeda/ddns/internal/application/core/domain" + "github.com/maximotejeda/ddns/internal/application/core/domain/cf" +) + +type result struct { + cf.Result +} + +// RootIdentify +// identify root domain from a list of dns records +// basically root will be example.com +// if splitted and > 2 then is a sub domain +// else if == 2 is root domain +func RootIdentify(domains []cf.Result) (roots map[string]dm.DomainRecords, err error) { + roots = map[string]dm.DomainRecords{} + for _, reg := range domains { + name := reg.Name + nameParts := strings.Split(name, ".") + + // if the name is equal to the root actual name continue + if len(nameParts) > 2 { + r := strings.Join(nameParts[len(nameParts)-2:], ".") + if _, ok := roots[r]; !ok { + roots[r] = dm.DomainRecords{Domain: r} + } + if record, ok := roots[r]; ok { + record.Existing = append(record.Existing, dm.Record{ID: reg.ID, Name: name, Type: reg.Type, Content: reg.Content}) + roots[r] = record + } + + } else if len(nameParts) == 2 { + if _, ok := roots[reg.Name]; !ok { + roots[reg.Name] = dm.DomainRecords{Domain: reg.Name} + } + if record, ok := roots[reg.Name]; ok { + record.Domain = reg.Name + record.Existing = append(record.Existing, dm.Record{ID: reg.ID, Name: name, Type: reg.Type, Content: reg.Content}) + roots[reg.Name] = record + } else { + fmt.Println(roots[reg.Name]) + } + } else { + continue + } + + } + return roots, err +} + +// SubDomainIdentify +// Identify subdomains from a list of subdomains +func SubdomainIdentify(name string, domains []cf.Result) (res map[string]dm.DomainRecords, err error) { + res, _ = RootIdentify(domains) + // check if name contains domain + // check if root domain list is in name + for k, r := range res { + if strings.Contains(name, r.Domain) { // if domain is on subname + if !slices.ContainsFunc(r.Existing, func(x dm.Record) bool { + + return x.Name == name + }) { + r.ToCreate = append(r.ToCreate, dm.Record{Name: name}) + res[k] = r + } else { + fmt.Println(slices.ContainsFunc(r.Existing, func(x dm.Record) bool { + return x.Name == name + })) + } + } else { + r.ToCreate = append(r.ToCreate, dm.Record{Name: fmt.Sprintf("%s.%s", name, r.Domain)}) + res[k] = r + } + } + return res, nil +} + +// SelectedDomain +// return an string +func SelectDomain(name string, domains map[string]dm.DomainRecords) string { + for k, r := range domains { + if name == "*" { + return name + } + if strings.Contains(name, k) { // if domain is on subname + return name + } else { + if err := domain.Check(name); err != nil { + panic(err) + } + return fmt.Sprintf("%s.%s", name, r.Domain) + } + } + return "" +} diff --git a/internal/port/checker.go b/internal/port/checker.go new file mode 100644 index 0000000..38375cc --- /dev/null +++ b/internal/port/checker.go @@ -0,0 +1,5 @@ +package port + +// Checker +// Type for query ip +type Checker interface{} diff --git a/internal/port/provider.go b/internal/port/provider.go new file mode 100644 index 0000000..94c478e --- /dev/null +++ b/internal/port/provider.go @@ -0,0 +1,19 @@ +package port + +import ( + "io" + + "github.com/maximotejeda/ddns/internal/application/core/domain/cf" +) + +type Provider interface { + List() (result *cf.ResponseBody, err error) + Export() ([]byte, error) + Import(file io.Reader) (err error) + Update(id string, reqBody *cf.RequestBody) (result *cf.DetailsResult, err error) + Create(reqBody *cf.RequestBody) (result *cf.DetailsResult, err error) + Delete(id string) (result *cf.DetailsResult, err error) + Overwrite(reqBody cf.RequestBody, id string) (result *cf.DetailsResult, err error) + Details(id string) (result *cf.DetailsResult, err error) + Selector(subDomain, domain, comment, tipe string) (selRecord []*cf.Result, err error) +}