FIRST COMMIT
This commit is contained in:
commit
d96142b348
0
.dir-locals.el
Normal file
0
.dir-locals.el
Normal file
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/.env
|
||||
bin/*
|
||||
14
Makefile
Normal file
14
Makefile
Normal file
@ -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
|
||||
65
README.org
Normal file
65
README.org
Normal file
@ -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:[]} |
|
||||
101
cmd/main.go
Normal file
101
cmd/main.go
Normal file
@ -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
|
||||
`
|
||||
}
|
||||
17
config/config.bak
Normal file
17
config/config.bak
Normal file
@ -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
|
||||
26
config/config.go
Normal file
26
config/config.go
Normal file
@ -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)
|
||||
}
|
||||
16
config/settings.yaml
Normal file
16
config/settings.yaml
Normal file
@ -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
|
||||
7
go.mod
Normal file
7
go.mod
Normal file
@ -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
|
||||
6
go.sum
Normal file
6
go.sum
Normal file
@ -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=
|
||||
95
internal/adapter/checker/checker.go
Normal file
95
internal/adapter/checker/checker.go
Normal file
@ -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
|
||||
}
|
||||
326
internal/adapter/provider/cloudflare.go
Normal file
326
internal/adapter/provider/cloudflare.go
Normal file
@ -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
|
||||
}
|
||||
14
internal/adapter/provider/cloudflare_test.go
Normal file
14
internal/adapter/provider/cloudflare_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
123
internal/api/domain/cf/cloudflare.go
Normal file
123
internal/api/domain/cf/cloudflare.go
Normal file
@ -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"`
|
||||
}
|
||||
304
internal/application/core/api/api.go
Normal file
304
internal/application/core/api/api.go
Normal file
@ -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
|
||||
}
|
||||
124
internal/application/core/domain/cf/cloudflare.go
Normal file
124
internal/application/core/domain/cf/cloudflare.go
Normal file
@ -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"`
|
||||
}
|
||||
20
internal/application/core/domain/provider.go
Normal file
20
internal/application/core/domain/provider.go
Normal file
@ -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)
|
||||
}
|
||||
102
internal/application/core/helpers/helpers.go
Normal file
102
internal/application/core/helpers/helpers.go
Normal file
@ -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 ""
|
||||
}
|
||||
5
internal/port/checker.go
Normal file
5
internal/port/checker.go
Normal file
@ -0,0 +1,5 @@
|
||||
package port
|
||||
|
||||
// Checker
|
||||
// Type for query ip
|
||||
type Checker interface{}
|
||||
19
internal/port/provider.go
Normal file
19
internal/port/provider.go
Normal file
@ -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)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user